From 58edbb5d5e61f6dc630dd7bfd3a4b24bfe523f7b Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 8 Feb 2023 20:04:38 -0600 Subject: [PATCH 01/31] Started writing the checkpointing system --- contracts/Hyperdrive.sol | 88 ++++++++++++++++++++++++++++++++-------- test/Hyperdrive.t.sol | 1 + 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 826a79bb1..d0e665a9e 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -29,6 +29,9 @@ contract Hyperdrive is MultiToken { /// Time /// + // @dev 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. uint256 public immutable positionDuration; @@ -40,33 +43,38 @@ contract Hyperdrive is MultiToken { // @dev The share price at the time the pool was created. uint256 public immutable initialSharePrice; - // @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. + // TODO: We'll likely need to add more information to these checkpoints. + // + /// @dev 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. 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. + /// @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. uint256 public bondReserves; - // @notice The amount of longs that are still open. + /// @notice The amount of longs that are still open. uint256 public longsOutstanding; - // @notice The amount of shorts that are still open. + /// @notice The amount of shorts that are still open. uint256 public shortsOutstanding; - // @notice The amount of long withdrawal shares that haven't been paid out. + /// @notice The amount of long withdrawal shares that haven't been paid out. uint256 public longWithdrawalSharesOutstanding; - // @notice The amount of short withdrawal shares that haven't been paid out. + /// @notice The amount of short withdrawal shares that haven't been paid out. uint256 public shortWithdrawalSharesOutstanding; - // @notice The proceeds that have accrued to the long withdrawal shares. + /// @notice The proceeds that have accrued to the long withdrawal shares. uint256 public longWithdrawalShareProceeds; - // @notice The proceeds that have accrued to the short withdrawal shares. + /// @notice The proceeds that have accrued to the short withdrawal shares. uint256 public shortWithdrawalShareProceeds; /// @notice Initializes a Hyperdrive pool. @@ -75,13 +83,16 @@ contract Hyperdrive is MultiToken { /// @param _linkerFactory The factory which is used to deploy the ERC20 /// linker contracts. /// @param _baseToken The base token contract. - /// @param _positionDuration The time in seconds that elapses before bonds + /// @param _checkpointDuration The time in seconds between share price + /// checkpoints. + /// @param _positionDuration The time in seconds that elaspes before bonds /// can be redeemed one-to-one for base. /// @param _timeStretch The time stretch of the pool. constructor( bytes32 _linkerCodeHash, address _linkerFactory, IERC20 _baseToken, + uint256 _checkpointDuration, uint256 _positionDuration, uint256 _timeStretch, uint256 _initialPricePerShare @@ -89,11 +100,20 @@ contract Hyperdrive is MultiToken { // Initialize the base token address. baseToken = _baseToken; + // FIXME: Ensure that the position duration is a multiple of the + // checkpoint duration. + // // Initialize the time configurations. + checkpointDuration = _checkpointDuration; positionDuration = _positionDuration; timeStretch = _timeStretch; + // Initialize the share prices. initialSharePrice = _initialPricePerShare; + // TODO: Use the update checkpoint helper function. + checkpoints[ + block.timestamp - (block.timestamp % checkpointDuration) + ] = sharePrice; } /// Yield Source /// @@ -297,11 +317,25 @@ contract Hyperdrive is MultiToken { revert Errors.ZeroAmount(); } - // Take custody of the base that is being traded into the contract. + // Perform a checkpoint and compute the amount of interest the long + // would have paid had they opened at the beginning of the checkpoint. + // To ensure that our PnL accounting works correctly, longs must pay for + // this backdated interest. + (uint256 latestCheckpoint, uint256 openSharePrice) = _checkpoint(); + // (c_1 - c_0) * (dx / c_0) = (c_1 / c_0 - 1) * dx + uint256 owedInterest = (sharePrice.divDown(openSharePrice) - + FixedPointMath.ONE_18).mulDown(_baseAmount); + uint256 maturityDate = latestCheckpoint + positionDuration; + uint256 timeRemaining = (maturityDate - block.timestamp).divDown( + positionDuration + ); + + // Take custody of the base that is being traded into the contract and + // the interest owed on the backdated bonds. bool success = baseToken.transferFrom( msg.sender, address(this), - _baseAmount + _baseAmount.add(owedInterest) ); if (!success) { revert Errors.TransferFailed(); @@ -317,7 +351,7 @@ contract Hyperdrive is MultiToken { bondReserves, totalSupply[AssetId._LP_ASSET_ID], shareAmount, - FixedPointMath.ONE_18, + timeRemaining, timeStretch, sharePrice, initialSharePrice, @@ -366,6 +400,9 @@ contract Hyperdrive is MultiToken { revert Errors.ZeroAmount(); } + // Perform a checkpoint. + _checkpoint(); + // Burn the longs that are being closed. uint256 assetId = AssetId.encodeAssetId( AssetId.AssetIdPrefix.Long, @@ -685,4 +722,21 @@ contract Hyperdrive is MultiToken { timeStretch ); } + + // TODO: We need to pay out the withdrawal pools in this function. + // + // TODO: Comment this. + function _checkpoint() + internal + returns (uint256 latestCheckpoint, uint256 openSharePrice) + { + latestCheckpoint = + block.timestamp - + (block.timestamp % checkpointDuration); + if (checkpoints[latestCheckpoint] == 0) { + checkpoints[latestCheckpoint] = sharePrice; + return (latestCheckpoint, sharePrice); + } + return (latestCheckpoint, checkpoints[latestCheckpoint]); + } } diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index c6bbc2b09..1f48d2bc6 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -31,6 +31,7 @@ contract HyperdriveTest is Test { linkerCodeHash, address(forwarderFactory), baseToken, + 1 days, 365 days, 22.186877016851916266e18, FixedPointMath.ONE_18 From bb09b5abeca44177fea95af19f139339c0400b8e Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 9 Feb 2023 15:32:05 -0600 Subject: [PATCH 02/31] Wrote checkpointing logic without making it zombie-proof --- contracts/Hyperdrive.sol | 164 +++++++++++++++++++++------------ contracts/libraries/Errors.sol | 1 + test/Hyperdrive.t.sol | 6 +- 3 files changed, 109 insertions(+), 62 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index d0e665a9e..5ad970d83 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -83,33 +83,35 @@ contract Hyperdrive is MultiToken { /// @param _linkerFactory The factory which is used to deploy the ERC20 /// linker contracts. /// @param _baseToken The base token contract. - /// @param _checkpointDuration The time in seconds between share price - /// checkpoints. + /// @param _initialSharePrice The initial share price. /// @param _positionDuration The time in seconds that elaspes before bonds /// can be redeemed one-to-one for base. + /// @param _checkpointDuration The time in seconds between share price + /// checkpoints. Position duration must be a multiple of checkpoint + /// duration. /// @param _timeStretch The time stretch of the pool. constructor( bytes32 _linkerCodeHash, address _linkerFactory, IERC20 _baseToken, - uint256 _checkpointDuration, - uint256 _positionDuration, - uint256 _timeStretch, uint256 _initialPricePerShare + uint256 _positionDuration, + uint256 _checkpointDuration, + uint256 _timeStretch ) MultiToken(_linkerCodeHash, _linkerFactory) { // Initialize the base token address. baseToken = _baseToken; - // FIXME: Ensure that the position duration is a multiple of the - // checkpoint duration. - // // Initialize the time configurations. - checkpointDuration = _checkpointDuration; + if (_positionDuration % _checkpointDuration != 0) { + revert Errors.InvalidCheckpointDuration(); + } positionDuration = _positionDuration; + checkpointDuration = _checkpointDuration; timeStretch = _timeStretch; // Initialize the share prices. - initialSharePrice = _initialPricePerShare; + initialSharePrice = _initialSharePrice; // TODO: Use the update checkpoint helper function. checkpoints[ block.timestamp - (block.timestamp % checkpointDuration) @@ -161,15 +163,21 @@ contract Hyperdrive is MultiToken { } // Deposit for the user, this transfers from them. - (uint256 shares, uint256 pricePerShare) = deposit(_contribution); + (uint256 shares, uint256 sharePrice) = deposit(_contribution); + + // Create an initial checkpoint. + _checkpoint(); + + // Calculate the amount of LP shares the initializer receives. + uint256 lpShares = shareReserves; - // Set the state variables. + // Update the reserves. The bond reserves are calculated so that the + // pool is initialized with the target APR. shareReserves = shares; - // We calculate the implied bond reserve from the share price bondReserves = HyperdriveMath.calculateBondReserves( shares, shares, - pricePerShare, + sharePrice, _apr, positionDuration, timeStretch @@ -317,33 +325,29 @@ contract Hyperdrive is MultiToken { revert Errors.ZeroAmount(); } - // Perform a checkpoint and compute the amount of interest the long - // would have paid had they opened at the beginning of the checkpoint. - // To ensure that our PnL accounting works correctly, longs must pay for - // this backdated interest. - (uint256 latestCheckpoint, uint256 openSharePrice) = _checkpoint(); - // (c_1 - c_0) * (dx / c_0) = (c_1 / c_0 - 1) * dx - uint256 owedInterest = (sharePrice.divDown(openSharePrice) - - FixedPointMath.ONE_18).mulDown(_baseAmount); - uint256 maturityDate = latestCheckpoint + positionDuration; - uint256 timeRemaining = (maturityDate - block.timestamp).divDown( - positionDuration - ); + // Perform a checkpoint. + (uint256 latestCheckpoint, ) = _checkpoint(); // Take custody of the base that is being traded into the contract and - // the interest owed on the backdated bonds. + // bool success = baseToken.transferFrom( msg.sender, address(this), - _baseAmount.add(owedInterest) + _baseAmount ); if (!success) { revert Errors.TransferFailed(); } - // Load price per share from the yield source + // Calculate the pool and user deltas using the trading function. We + // backdate the bonds purchased to the beginning of the checkpoint. We + // reduce the purchasing power of the longs by the amount of interest + // earned in shares. uint256 sharePrice = _pricePerShare(); - // Calculate the pool and user deltas using the trading function. + uint256 maturityTime = latestCheckpoint + positionDuration; + uint256 timeRemaining = (maturityTime - block.timestamp).divDown( + positionDuration + ); uint256 shareAmount = _baseAmount.divDown(sharePrice); (, uint256 poolBondDelta, uint256 bondProceeds) = HyperdriveMath .calculateOutGivenIn( @@ -380,7 +384,7 @@ contract Hyperdrive is MultiToken { AssetId.encodeAssetId( AssetId.AssetIdPrefix.Long, sharePrice, - block.timestamp + positionDuration + maturityTime ), msg.sender, bondProceeds @@ -412,10 +416,8 @@ contract Hyperdrive is MultiToken { _burn(assetId, msg.sender, _bondAmount); longsOutstanding -= _bondAmount; - // Load the price per share - uint256 sharePrice = _pricePerShare(); - // Calculate the pool and user deltas using the trading function. + uint256 sharePrice = _pricePerShare(); uint256 timeRemaining = block.timestamp < uint256(_maturityTime) ? (uint256(_maturityTime) - block.timestamp).divDown( positionDuration @@ -434,6 +436,9 @@ contract Hyperdrive is MultiToken { false ); + // FIXME: This behavior may need to be revised when we update the + // automatic closing flow. + // // If there are outstanding long withdrawal shares, we attribute a // proportional amount of the proceeds to the withdrawal pool and the // active LPs. Otherwise, we use simplified accounting that has the same @@ -441,12 +446,20 @@ contract Hyperdrive is MultiToken { // base reserves and the longs outstanding stays the same or gets // larger, we don't need to verify the reserves invariants. if (longWithdrawalSharesOutstanding > 0) { + // Since longs are backdated to the beginning of the checkpoint and + // interest only begins accruing when the longs are opened, we + // exclude the first checkpoint from LP withdrawal payouts. For most + // pools the difference will not be meaningful, and in edge cases, + // fees can be tuned to offset the problem. + uint256 openSharePrice = checkpoints[ + (_maturityTime - positionDuration) + checkpointDuration + ]; _applyCloseLong( _bondAmount, poolBondDelta, shareProceeds, - _openSharePrice, - sharePrice + sharePrice, + openSharePrice ); } else { shareReserves -= shareProceeds; @@ -472,30 +485,48 @@ contract Hyperdrive is MultiToken { revert Errors.ZeroAmount(); } - // Load the share price at the current block - uint256 sharePrice = _pricePerShare(); + // Perform a checkpoint and compute the amount of interest the short + // would have received if they opened at the beginning of the checkpoint. + // Since the short will receive interest from the beginning of the + // checkpoint, they will receive this backdated interest back at closing. + (uint256 latestCheckpoint, uint256 openSharePrice) = _checkpoint(); - // Calculate the pool and user deltas using the trading function. + // Calculate the pool and user deltas using the trading function. We + // backdate the bonds sold to the beginning of the checkpoint. + uint256 sharePrice = _pricePerShare(); + uint256 maturityTime = latestCheckpoint + positionDuration; + uint256 timeRemaining = (maturityTime - block.timestamp).divDown( + positionDuration + ); (uint256 poolShareDelta, , uint256 shareProceeds) = HyperdriveMath .calculateOutGivenIn( shareReserves, bondReserves, totalSupply[AssetId._LP_ASSET_ID], _bondAmount, - FixedPointMath.ONE_18, + timeRemaining, timeStretch, sharePrice, initialSharePrice, false ); - // Take custody of the maximum amount the trader can lose on the short. - // And deposit it into the yield source + // Take custody of the maximum amount the trader can lose on the short + // and the extra interest the short will receive at closing (since the + // proceeds of the trades are calculated using the checkpoint's open + // share price). This extra interest can be calculated as: + // + // interest = (c_1 - c_0) * (dy / c_0) + // = (c_1 / c_0 - 1) * dy + uint256 owedInterest = (sharePrice.divDown(openSharePrice) - + FixedPointMath.ONE_18).mulDown(_bondAmount); uint256 baseProceeds = shareProceeds.mulDown(sharePrice); - deposit(_bondAmount - baseProceeds); + deposit((_bondAmount - baseProceeds) + owedInterest); // max_loss + interest // Apply the trading deltas to the reserves and increase the bond buffer - // by the amount of bonds that were shorted. + // 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 + // the close short accounting works. shareReserves -= poolShareDelta; bondReserves += _bondAmount; shortsOutstanding += _bondAmount; @@ -512,7 +543,7 @@ contract Hyperdrive is MultiToken { AssetId.encodeAssetId( AssetId.AssetIdPrefix.Short, sharePrice, - block.timestamp + positionDuration + maturityTime ), msg.sender, _bondAmount @@ -532,6 +563,9 @@ contract Hyperdrive is MultiToken { revert Errors.ZeroAmount(); } + // Perform a checkpoint. + _checkpoint(); + // Burn the shorts that are being closed. uint256 assetId = AssetId.encodeAssetId( AssetId.AssetIdPrefix.Short, @@ -565,6 +599,9 @@ contract Hyperdrive is MultiToken { initialSharePrice ); + // FIXME: This behavior may need to be revised when we update the + // automatic closing flow. + // // If there are outstanding short withdrawal shares, we attribute a // proportional amount of the proceeds to the withdrawal pool and the // active LPs. Otherwise, we use simplified accounting that has the same @@ -583,19 +620,22 @@ contract Hyperdrive is MultiToken { bondReserves -= poolBondDelta; } - // Convert the bonds to current shares - uint256 _bondsInShares = _bondAmount.divDown(sharePrice); + // TODO: Double check this math. + // // Transfer the profit to the shorter. This includes the proceeds from // the short sale as well as the variable interest that was collected - // on the face value of the bonds. The math for the short's proceeds is - // given by: + // on the face value of the bonds. The math for the short's proceeds in + // base is given by: // - // c * (dy / c_0 - dz) - uint256 shortProceeds = ( - _bondsInShares.mulDown(sharePrice.divDown(_openSharePrice)).sub( - sharePayment - ) - ); + // proceeds = dy - c * dz + (c - c_0) * (dy / c_0) + // = dy - c * dz + (c / c_0) * dy - dy + // = (c / c_0) * dy - c * dz + // = c * (dy / c_0 - dz) + // + // To convert to proceeds in shares, we simply divide by the current + // share price. + uint256 openSharePrice = checkpoints[_maturityTime - positionDuration]; + uint256 shortProceeds = _bondAmount.divDown(openSharePrice).sub(sharePayment); // Withdraw from the reserves // TODO - Better destination support withdraw(shortProceeds, msg.sender); @@ -611,14 +651,14 @@ contract Hyperdrive is MultiToken { /// pool. /// @param _shareProceeds The proceeds in shares received from closing the /// long. + /// @param _sharePrice The current share price. /// @param _openSharePrice The share price at the time the long was opened. - /// @param _sharePrice The current share price function _applyCloseLong( uint256 _bondAmount, uint256 _poolBondDelta, uint256 _shareProceeds, - uint256 _openSharePrice, - uint256 _sharePrice + uint256 _sharePrice, + uint256 _openSharePrice ) internal { // Calculate the effect that the trade has on the pool's APR. uint256 apr = HyperdriveMath.calculateAPRFromReserves( @@ -636,7 +676,7 @@ contract Hyperdrive is MultiToken { // when longs are opened. The math for the withdrawal proceeds is given // by: // - // c * (dy / c_0 - dz) * (min(b_x, dy) / dy) + // proceeds = c * (dy / c_0 - dz) * (min(b_x, dy) / dy) uint256 withdrawalAmount = longWithdrawalSharesOutstanding < _bondAmount ? longWithdrawalSharesOutstanding : _bondAmount; @@ -694,7 +734,7 @@ contract Hyperdrive is MultiToken { // shorts are opened. The math for the withdrawal proceeds is given // by: // - // c * dz * (min(b_y, dy) / dy) + // proceeds = c * dz * (min(b_y, dy) / dy) uint256 withdrawalAmount = shortWithdrawalSharesOutstanding < _bondAmount ? shortWithdrawalSharesOutstanding @@ -725,7 +765,9 @@ contract Hyperdrive is MultiToken { // TODO: We need to pay out the withdrawal pools in this function. // - // TODO: Comment this. + /// @dev Creates a new checkpoint if necessary. + /// @return latestCheckpoint The latest checkpoint time. + /// @return openSharePrice The open share price of the latest checkpoint. function _checkpoint() internal returns (uint256 latestCheckpoint, uint256 openSharePrice) diff --git a/contracts/libraries/Errors.sol b/contracts/libraries/Errors.sol index bf3d3887e..3836acc6e 100644 --- a/contracts/libraries/Errors.sol +++ b/contracts/libraries/Errors.sol @@ -12,6 +12,7 @@ library Errors { /// ### Hyperdrive ### /// ################## error BaseBufferExceedsShareReserves(); + error InvalidCheckpointDuration(); error InvalidMaturityTime(); error PoolAlreadyInitialized(); error TransferFailed(); diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index 1f48d2bc6..c21f760aa 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -31,10 +31,14 @@ contract HyperdriveTest is Test { linkerCodeHash, address(forwarderFactory), baseToken, - 1 days, 365 days, +<<<<<<< HEAD 22.186877016851916266e18, FixedPointMath.ONE_18 +======= + 1 days, + 22.186877016851916266e18 +>>>>>>> 08a3149 (Wrote checkpointing logic without making it zombie-proof) ); } } From 64aeba6ea3820eb75964c53940353237392313b7 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 10 Feb 2023 14:01:40 -0600 Subject: [PATCH 03/31] Fixed changes after rebase --- contracts/AaveYieldSource.sol | 31 ++++---- contracts/Hyperdrive.sol | 134 ++++++++++++++-------------------- test/Hyperdrive.t.sol | 22 +++--- 3 files changed, 78 insertions(+), 109 deletions(-) diff --git a/contracts/AaveYieldSource.sol b/contracts/AaveYieldSource.sol index f6c3f5834..622eca821 100644 --- a/contracts/AaveYieldSource.sol +++ b/contracts/AaveYieldSource.sol @@ -33,12 +33,16 @@ contract AaveYieldSource is Hyperdrive { /// @param _baseToken The base token contract. /// @param _positionDuration The time in seconds that elapses before bonds /// can be redeemed one-to-one for base. + /// @param _checkpointDuration The time in seconds between share price + /// checkpoints. Position duration must be a multiple of checkpoint + /// duration. /// @param _timeStretch The time stretch of the pool. constructor( bytes32 _linkerCodeHash, address _linkerFactory, IERC20 _baseToken, uint256 _positionDuration, + uint256 _checkpointDuration, uint256 _timeStretch, IERC20 _aToken, Pool _pool @@ -47,9 +51,10 @@ contract AaveYieldSource is Hyperdrive { _linkerCodeHash, _linkerFactory, _baseToken, + FixedPointMath.ONE_18, _positionDuration, - _timeStretch, - FixedPointMath.ONE_18 + _checkpointDuration, + _timeStretch ) { aToken = _aToken; @@ -59,10 +64,10 @@ contract AaveYieldSource is Hyperdrive { ///@notice Transfers amount of 'token' from the user and commits it to the yield source. ///@param amount The amount of token to transfer ///@return sharesMinted The shares this deposit creates - ///@return pricePerShare The price per share at time of deposit + ///@return sharePrice The share price at time of deposit function deposit( uint256 amount - ) internal override returns (uint256 sharesMinted, uint256 pricePerShare) { + ) internal override returns (uint256 sharesMinted, uint256 sharePrice) { // Transfer from user bool success = baseToken.transferFrom( msg.sender, @@ -93,15 +98,11 @@ contract AaveYieldSource is Hyperdrive { ///@param shares The shares to withdraw from the yieldsource ///@param destination The address which is where to send the resulting tokens ///@return amountWithdrawn the amount of 'token' produced by this withdraw - ///@return pricePerShare The price per share on withdraw. + ///@return sharePrice The share price on withdraw. function withdraw( uint256 shares, address destination - ) - internal - override - returns (uint256 amountWithdrawn, uint256 pricePerShare) - { + ) internal override returns (uint256 amountWithdrawn, uint256 sharePrice) { // Load the balance of this contract uint256 assets = aToken.balanceOf(address(this)); // The withdraw is the percent of shares the user has times the total assets @@ -112,17 +113,17 @@ contract AaveYieldSource is Hyperdrive { return (withdrawValue, shares.divDown(withdrawValue)); } - ///@notice Loads the price per share from the yield source - ///@return pricePerShare The current price per share - function _pricePerShare() + ///@notice Loads the share price from the yield source. + ///@return sharePrice The current share price. + function pricePerShare() internal view override - returns (uint256 pricePerShare) + returns (uint256 sharePrice) { // Load the balance of this contract uint256 assets = aToken.balanceOf(address(this)); - // Price per share is assets divided by shares + // The share price is assets divided by shares return (assets.divDown(totalShares)); } } diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 5ad970d83..0f56b216f 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -80,8 +80,8 @@ contract Hyperdrive is MultiToken { /// @notice Initializes a Hyperdrive pool. /// @param _linkerCodeHash The hash of the ERC20 linker contract's /// constructor code. - /// @param _linkerFactory The factory which is used to deploy the ERC20 - /// linker contracts. + /// @param _linkerFactoryAddress The address of the factory which is used to + /// deploy the ERC20 linker contracts. /// @param _baseToken The base token contract. /// @param _initialSharePrice The initial share price. /// @param _positionDuration The time in seconds that elaspes before bonds @@ -92,13 +92,13 @@ contract Hyperdrive is MultiToken { /// @param _timeStretch The time stretch of the pool. constructor( bytes32 _linkerCodeHash, - address _linkerFactory, + address _linkerFactoryAddress, IERC20 _baseToken, - uint256 _initialPricePerShare + uint256 _initialSharePrice, uint256 _positionDuration, uint256 _checkpointDuration, uint256 _timeStretch - ) MultiToken(_linkerCodeHash, _linkerFactory) { + ) MultiToken(_linkerCodeHash, _linkerFactoryAddress) { // Initialize the base token address. baseToken = _baseToken; @@ -112,44 +112,33 @@ contract Hyperdrive is MultiToken { // Initialize the share prices. initialSharePrice = _initialSharePrice; - // TODO: Use the update checkpoint helper function. - checkpoints[ - block.timestamp - (block.timestamp % checkpointDuration) - ] = sharePrice; } /// Yield Source /// // In order to deploy a yield source implement must be written which implements the following methods - ///@notice Transfers amount of 'token' from the user and commits it to the yield source. - ///@param amount The amount of token to transfer - ///@return sharesMinted The shares this deposit creates - ///@return pricePerShare The price per share at time of deposit + /// @notice Transfers base from the user and commits it to the yield source. + /// @param amount The amount of base to deposit. + /// @return sharesMinted The shares this deposit creates. + /// @return sharePrice The share price at time of deposit. function deposit( uint256 amount - ) internal virtual returns (uint256 sharesMinted, uint256 pricePerShare) {} - - ///@notice Withdraws shares from the yield source and sends the resulting tokens to the destination - ///@param shares The shares to withdraw from the yieldsource - ///@param destination The address which is where to send the resulting tokens - ///@return amountWithdrawn the amount of 'token' produced by this withdraw - ///@return pricePerShare The price per share on withdraw. + ) internal virtual returns (uint256 sharesMinted, uint256 sharePrice) {} + + /// @notice Withdraws shares from the yield source and sends the base + /// released to the destination. + /// @param shares The shares to withdraw from the yieldsource. + /// @param destination The recipient of the withdrawal. + /// @return amountWithdrawn The amount of base released by the withdrawal. + /// @return sharePrice The share price on withdraw. function withdraw( uint256 shares, address destination - ) - internal - virtual - returns (uint256 amountWithdrawn, uint256 pricePerShare) - {} - - ///@notice Loads the price per share from the yield source - ///@return pricePerShare The current price per share - function _pricePerShare() - internal - virtual - returns (uint256 pricePerShare) - {} + ) internal virtual returns (uint256 amountWithdrawn, uint256 sharePrice) {} + + ///@notice Loads the share price from the yield source + ///@return sharePrice The current share price. + function pricePerShare() internal virtual returns (uint256 sharePrice) {} /// LP /// @@ -166,10 +155,7 @@ contract Hyperdrive is MultiToken { (uint256 shares, uint256 sharePrice) = deposit(_contribution); // Create an initial checkpoint. - _checkpoint(); - - // Calculate the amount of LP shares the initializer receives. - uint256 lpShares = shareReserves; + _checkpoint(sharePrice); // Update the reserves. The bond reserves are calculated so that the // pool is initialized with the target APR. @@ -199,7 +185,7 @@ contract Hyperdrive is MultiToken { } // Deposit for the user, this call also transfers from them - (uint256 shares, uint256 pricePerShare) = deposit(_contribution); + (uint256 shares, uint256 sharePrice) = deposit(_contribution); // Calculate the pool's APR prior to updating the share reserves so that // we can compute the bond reserves update. @@ -219,7 +205,7 @@ contract Hyperdrive is MultiToken { totalSupply[AssetId._LP_ASSET_ID], longsOutstanding, shortsOutstanding, - pricePerShare + sharePrice ); // Update the reserves. @@ -270,7 +256,7 @@ contract Hyperdrive is MultiToken { totalSupply[AssetId._LP_ASSET_ID], longsOutstanding, shortsOutstanding, - _pricePerShare() + pricePerShare() ); // Burn the LP shares. @@ -325,36 +311,26 @@ contract Hyperdrive is MultiToken { revert Errors.ZeroAmount(); } - // Perform a checkpoint. - (uint256 latestCheckpoint, ) = _checkpoint(); + // Deposit the user's base. + (uint256 shares, uint256 sharePrice) = deposit(_baseAmount); - // Take custody of the base that is being traded into the contract and - // - bool success = baseToken.transferFrom( - msg.sender, - address(this), - _baseAmount - ); - if (!success) { - revert Errors.TransferFailed(); - } + // Perform a checkpoint. + (uint256 latestCheckpoint, ) = _checkpoint(sharePrice); // Calculate the pool and user deltas using the trading function. We // backdate the bonds purchased to the beginning of the checkpoint. We // reduce the purchasing power of the longs by the amount of interest // earned in shares. - uint256 sharePrice = _pricePerShare(); uint256 maturityTime = latestCheckpoint + positionDuration; uint256 timeRemaining = (maturityTime - block.timestamp).divDown( positionDuration ); - uint256 shareAmount = _baseAmount.divDown(sharePrice); (, uint256 poolBondDelta, uint256 bondProceeds) = HyperdriveMath .calculateOutGivenIn( shareReserves, bondReserves, totalSupply[AssetId._LP_ASSET_ID], - shareAmount, + shares, timeRemaining, timeStretch, sharePrice, @@ -364,7 +340,7 @@ contract Hyperdrive is MultiToken { // Apply the trading deltas to the reserves and update the amount of // longs outstanding. - shareReserves += shareAmount; + shareReserves += shares; bondReserves -= poolBondDelta; longsOutstanding += bondProceeds; @@ -405,7 +381,8 @@ contract Hyperdrive is MultiToken { } // Perform a checkpoint. - _checkpoint(); + uint256 sharePrice = pricePerShare(); + _checkpoint(sharePrice); // Burn the longs that are being closed. uint256 assetId = AssetId.encodeAssetId( @@ -417,7 +394,6 @@ contract Hyperdrive is MultiToken { longsOutstanding -= _bondAmount; // Calculate the pool and user deltas using the trading function. - uint256 sharePrice = _pricePerShare(); uint256 timeRemaining = block.timestamp < uint256(_maturityTime) ? (uint256(_maturityTime) - block.timestamp).divDown( positionDuration @@ -466,14 +442,9 @@ contract Hyperdrive is MultiToken { bondReserves += poolBondDelta; } - // Transfer the base returned to the trader. - bool success = baseToken.transfer( - msg.sender, - shareProceeds.mulDown(sharePrice) - ); - if (!success) { - revert Errors.TransferFailed(); - } + // Withdraw the profit to the trader. + // TODO: Better destination support. + withdraw(shareProceeds, msg.sender); } /// Short /// @@ -489,11 +460,13 @@ contract Hyperdrive is MultiToken { // would have received if they opened at the beginning of the checkpoint. // Since the short will receive interest from the beginning of the // checkpoint, they will receive this backdated interest back at closing. - (uint256 latestCheckpoint, uint256 openSharePrice) = _checkpoint(); + uint256 sharePrice = pricePerShare(); + (uint256 latestCheckpoint, uint256 openSharePrice) = _checkpoint( + sharePrice + ); // Calculate the pool and user deltas using the trading function. We // backdate the bonds sold to the beginning of the checkpoint. - uint256 sharePrice = _pricePerShare(); uint256 maturityTime = latestCheckpoint + positionDuration; uint256 timeRemaining = (maturityTime - block.timestamp).divDown( positionDuration @@ -564,7 +537,8 @@ contract Hyperdrive is MultiToken { } // Perform a checkpoint. - _checkpoint(); + uint256 sharePrice = pricePerShare(); + _checkpoint(sharePrice); // Burn the shorts that are being closed. uint256 assetId = AssetId.encodeAssetId( @@ -575,9 +549,6 @@ contract Hyperdrive is MultiToken { _burn(assetId, msg.sender, _bondAmount); shortsOutstanding -= _bondAmount; - // Load the share price - uint256 sharePrice = _pricePerShare(); - // Calculate the pool and user deltas using the trading function. uint256 timeRemaining = block.timestamp < uint256(_maturityTime) ? (uint256(_maturityTime) - block.timestamp).divDown( @@ -622,7 +593,7 @@ contract Hyperdrive is MultiToken { // TODO: Double check this math. // - // Transfer the profit to the shorter. This includes the proceeds from + // Withdraw the profit to the trader. This includes the proceeds from // the short sale as well as the variable interest that was collected // on the face value of the bonds. The math for the short's proceeds in // base is given by: @@ -635,8 +606,9 @@ contract Hyperdrive is MultiToken { // To convert to proceeds in shares, we simply divide by the current // share price. uint256 openSharePrice = checkpoints[_maturityTime - positionDuration]; - uint256 shortProceeds = _bondAmount.divDown(openSharePrice).sub(sharePayment); - // Withdraw from the reserves + uint256 shortProceeds = _bondAmount.divDown(openSharePrice).sub( + sharePayment + ); // TODO - Better destination support withdraw(shortProceeds, msg.sender); } @@ -766,18 +738,18 @@ contract Hyperdrive is MultiToken { // TODO: We need to pay out the withdrawal pools in this function. // /// @dev Creates a new checkpoint if necessary. + /// @param _sharePrice The current share price. /// @return latestCheckpoint The latest checkpoint time. /// @return openSharePrice The open share price of the latest checkpoint. - function _checkpoint() - internal - returns (uint256 latestCheckpoint, uint256 openSharePrice) - { + function _checkpoint( + uint256 _sharePrice + ) internal returns (uint256 latestCheckpoint, uint256 openSharePrice) { latestCheckpoint = block.timestamp - (block.timestamp % checkpointDuration); if (checkpoints[latestCheckpoint] == 0) { - checkpoints[latestCheckpoint] = sharePrice; - return (latestCheckpoint, sharePrice); + checkpoints[latestCheckpoint] = _sharePrice; + return (latestCheckpoint, _sharePrice); } return (latestCheckpoint, checkpoints[latestCheckpoint]); } diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index c21f760aa..b7cb4cdbf 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -27,18 +27,14 @@ contract HyperdriveTest is Test { ); // Instantiate Hyperdrive. - hyperdrive = new Hyperdrive( - linkerCodeHash, - address(forwarderFactory), - baseToken, - 365 days, -<<<<<<< HEAD - 22.186877016851916266e18, - FixedPointMath.ONE_18 -======= - 1 days, - 22.186877016851916266e18 ->>>>>>> 08a3149 (Wrote checkpointing logic without making it zombie-proof) - ); + hyperdrive = new Hyperdrive({ + _linkerCodeHash: linkerCodeHash, + _linkerFactoryAddress: address(forwarderFactory), + _baseToken: baseToken, + _initialSharePrice: FixedPointMath.ONE_18, + _positionDuration: 365 days, + _checkpointDuration: 1 days, + _timeStretch: 22.186877016851916266e18 + }); } } From ca84704ef0e3a28928fbccff25a79ecb5e33ca5a Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 10 Feb 2023 15:22:29 -0600 Subject: [PATCH 04/31] Added accounting for paying out matured longs to the withdrawal pool --- .solhint.json | 2 +- contracts/Hyperdrive.sol | 104 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/.solhint.json b/.solhint.json index e12cc82c4..a56c50ccb 100644 --- a/.solhint.json +++ b/.solhint.json @@ -6,7 +6,7 @@ "avoid-suicide": "error", "avoid-sha3": "warn", "reason-string": "off", - "private-vars-leading-underscore": "warn", + "private-vars-leading-underscore": "off", "compiler-version": ["off"], "func-visibility": ["warn",{"ignoreConstructors":true}], "not-rely-on-time": "off", diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 0f56b216f..ce383e2f4 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -15,7 +15,7 @@ import { MultiToken } from "contracts/MultiToken.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. - +// // TODO - Here we give default implementations of the virtual methods to not break tests // we should move to an abstract contract to prevent this from being deployed w/o // real implementations. @@ -65,6 +65,12 @@ contract Hyperdrive is MultiToken { /// @notice The amount of shorts that are still open. uint256 public shortsOutstanding; + /// @notice The amount of longs that have matured but have not been closed. + uint256 public longsMatured; + + /// @notice The amount of shorts that have matured but have not been closed. + uint256 public shortsMatured; + /// @notice The amount of long withdrawal shares that haven't been paid out. uint256 public longWithdrawalSharesOutstanding; @@ -744,6 +750,7 @@ contract Hyperdrive is MultiToken { function _checkpoint( uint256 _sharePrice ) internal returns (uint256 latestCheckpoint, uint256 openSharePrice) { + // Return early if the checkpoint has already been updated. latestCheckpoint = block.timestamp - (block.timestamp % checkpointDuration); @@ -751,6 +758,101 @@ contract Hyperdrive is MultiToken { checkpoints[latestCheckpoint] = _sharePrice; return (latestCheckpoint, _sharePrice); } + + // Pay out the long withdrawal pool for longs that have matured. + uint256 newlyMaturedLongs = totalSupply[ + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + 0, + latestCheckpoint + ) + ]; + if (newlyMaturedLongs > 0) { + // Since longs are backdated to the beginning of the checkpoint and + // interest only begins accruing when the longs are opened, we + // exclude the first checkpoint from LP withdrawal payouts. For most + // pools the difference will not be meaningful, and in edge cases, + // fees can be tuned to offset the problem. + uint256 openSharePrice = checkpoints[ + (latestCheckpoint - positionDuration) + checkpointDuration + ]; + _applyMaturedLongsPayout( + newlyMaturedLongs, + _sharePrice, + openSharePrice + ); + } + + // Pay out the short withdrawal pool for shorts that have matured. + if ( + totalSupply[ + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + 0, + latestCheckpoint + ) + ] > 0 + ) { + // FIXME + } + return (latestCheckpoint, checkpoints[latestCheckpoint]); } + + // FIXME: Update longsMatured here and elsewhere. + // + /// @dev Pays out the profits from matured longs to the withdrawal pool. + /// @param _bondAmount The amount of longs that have matured. + /// @param _sharePrice The current share price. + /// @param _openSharePrice The share price when the longs were opened. + function _applyMaturedLongsPayout( + uint256 _bondAmount, + uint256 _sharePrice, + uint256 _openSharePrice + ) internal { + // Calculate the current pool APR. + uint256 apr = HyperdriveMath.calculateAPRFromReserves( + shareReserves, + bondReserves, + totalSupply[AssetId._LP_ASSET_ID], + initialSharePrice, + positionDuration, + timeStretch + ); + + // Apply the LP proceeds from the trade proportionally to the long + // withdrawal shares. The accounting for these proceeds is identical + // to the close short accounting because LPs take the short position + // when longs are opened; however, we can make some simplifications + // since the longs have matured. The math for the withdrawal proceeds is + // given by: + // + // proceeds = c * (dy / c_0 - dy / c) * (min(b_x, dy) / dy) + // = (c / c_0 - 1) * dy * (min(b_x, dy) / dy) + // = (c / c_0 - 1) * min(b_x, dy) + uint256 withdrawalAmount = longWithdrawalSharesOutstanding < _bondAmount + ? longWithdrawalSharesOutstanding + : _bondAmount; + uint256 withdrawalProceeds = ( + _sharePrice.divDown(_openSharePrice).sub(FixedPointMath.ONE_18) + ).mulDown(withdrawalAmount); + longWithdrawalSharesOutstanding -= withdrawalAmount; + longWithdrawalShareProceeds += withdrawalProceeds; + + // Apply the withdrawal payouts to the reserves. These updates reflect + // the fact that some of the reserves will be attributed to the + // withdrawal pool. The math for the share reserves update is given by: + // + // z -= (c / c_0 - 1) * min(b_x, dy) / c + // = (1 / c_0 - 1 / c_1) * min(b_x, dy) + shareReserves -= withdrawalProceeds.divDown(_sharePrice); + bondReserves = HyperdriveMath.calculateBondReserves( + shareReserves, + totalSupply[AssetId._LP_ASSET_ID], + initialSharePrice, + apr, + positionDuration, + timeStretch + ); + } } From 46791723d354d2094fd050e138aecdfa7f129973 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 10 Feb 2023 16:19:54 -0600 Subject: [PATCH 05/31] Updated the close long accounting for matured longs --- contracts/Hyperdrive.sol | 239 ++++++++++++++----------- contracts/libraries/HyperdriveMath.sol | 19 +- 2 files changed, 145 insertions(+), 113 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index ce383e2f4..6e37f3af8 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -261,7 +261,9 @@ contract Hyperdrive is MultiToken { shareReserves, totalSupply[AssetId._LP_ASSET_ID], longsOutstanding, + longsMatured, shortsOutstanding, + shortsMatured, pricePerShare() ); @@ -397,7 +399,6 @@ contract Hyperdrive is MultiToken { _maturityTime ); _burn(assetId, msg.sender, _bondAmount); - longsOutstanding -= _bondAmount; // Calculate the pool and user deltas using the trading function. uint256 timeRemaining = block.timestamp < uint256(_maturityTime) @@ -418,9 +419,6 @@ contract Hyperdrive is MultiToken { false ); - // FIXME: This behavior may need to be revised when we update the - // automatic closing flow. - // // If there are outstanding long withdrawal shares, we attribute a // proportional amount of the proceeds to the withdrawal pool and the // active LPs. Otherwise, we use simplified accounting that has the same @@ -428,20 +426,12 @@ contract Hyperdrive is MultiToken { // base reserves and the longs outstanding stays the same or gets // larger, we don't need to verify the reserves invariants. if (longWithdrawalSharesOutstanding > 0) { - // Since longs are backdated to the beginning of the checkpoint and - // interest only begins accruing when the longs are opened, we - // exclude the first checkpoint from LP withdrawal payouts. For most - // pools the difference will not be meaningful, and in edge cases, - // fees can be tuned to offset the problem. - uint256 openSharePrice = checkpoints[ - (_maturityTime - positionDuration) + checkpointDuration - ]; _applyCloseLong( _bondAmount, poolBondDelta, shareProceeds, sharePrice, - openSharePrice + _maturityTime ); } else { shareReserves -= shareProceeds; @@ -553,7 +543,6 @@ contract Hyperdrive is MultiToken { _maturityTime ); _burn(assetId, msg.sender, _bondAmount); - shortsOutstanding -= _bondAmount; // Calculate the pool and user deltas using the trading function. uint256 timeRemaining = block.timestamp < uint256(_maturityTime) @@ -597,6 +586,9 @@ contract Hyperdrive is MultiToken { bondReserves -= poolBondDelta; } + // FIXME: We'll need to use a different share price if the short has + // already matured. + // // TODO: Double check this math. // // Withdraw the profit to the trader. This includes the proceeds from @@ -630,13 +622,13 @@ contract Hyperdrive is MultiToken { /// @param _shareProceeds The proceeds in shares received from closing the /// long. /// @param _sharePrice The current share price. - /// @param _openSharePrice The share price at the time the long was opened. + /// @param _maturityTime The maturity time of the longs. function _applyCloseLong( uint256 _bondAmount, uint256 _poolBondDelta, uint256 _shareProceeds, uint256 _sharePrice, - uint256 _openSharePrice + uint256 _maturityTime ) internal { // Calculate the effect that the trade has on the pool's APR. uint256 apr = HyperdriveMath.calculateAPRFromReserves( @@ -648,30 +640,128 @@ contract Hyperdrive is MultiToken { timeStretch ); + // Decrease the amount of longs outstanding. + longsOutstanding -= _bondAmount; + + // If the bonds have already matured, then the withdrawal proceeds have + // already been accounted for. Otherwise, we calculate the withdrawal + // proceeds and pay out the withdrawal pool. + uint256 withdrawalProceeds = 0; + if (_maturityTime > block.timestamp) { + // Since longs are backdated to the beginning of the checkpoint and + // interest only begins accruing when the longs are opened, we + // exclude the first checkpoint from LP withdrawal payouts. For most + // pools the difference will not be meaningful, and in edge cases, + // fees can be tuned to offset the problem. + uint256 openSharePrice = checkpoints[ + (_maturityTime - positionDuration) + checkpointDuration + ]; + + // Apply the LP proceeds from the trade proportionally to the long + // withdrawal shares. The accounting for these proceeds is identical + // to the close short accounting because LPs take the short position + // when longs are opened. The math for the withdrawal proceeds is given + // by: + // + // proceeds = c * (dy / c_0 - dz) * (min(b_x, dy) / dy) + uint256 withdrawalAmount = longWithdrawalSharesOutstanding < + _bondAmount + ? longWithdrawalSharesOutstanding + : _bondAmount; + withdrawalProceeds = _sharePrice + .mulDown( + _bondAmount.divDown(openSharePrice).sub(_shareProceeds) + ) + .mulDown(withdrawalAmount.divDown(_bondAmount)); + + // Update the long aggregates. + longWithdrawalSharesOutstanding -= withdrawalAmount; + longWithdrawalShareProceeds += withdrawalProceeds; + } else { + // Since the bonds have already matured, we've already paid out the + // withdrawal proceeds. All that needs to be done is to update the + // amount longs matured. + longsMatured -= _bondAmount; + } + + // Apply the trading deltas to the reserves. These updates reflect + // the fact that some of the reserves will be attributed to the + // withdrawal pool. Assuming that there are some withdrawal proceeds, + // the math for the share reserves update is given by: + // + // z -= dz + (dy / c_0 - dz) * (min(b_x, dy) / dy) + shareReserves -= _shareProceeds.add( + withdrawalProceeds.divDown(_sharePrice) + ); + bondReserves = HyperdriveMath.calculateBondReserves( + shareReserves, + totalSupply[AssetId._LP_ASSET_ID], + initialSharePrice, + apr, + positionDuration, + timeStretch + ); + } + + /// @dev Pays out the profits from matured longs to the withdrawal pool. + /// @param _bondAmount The amount of longs that have matured. + /// @param _sharePrice The current share price. + /// @param _maturityTime The maturity time of the longs. + function _applyMaturedLongsPayout( + uint256 _bondAmount, + uint256 _sharePrice, + uint256 _maturityTime + ) internal { + // Calculate the current pool APR. + uint256 apr = HyperdriveMath.calculateAPRFromReserves( + shareReserves, + bondReserves, + totalSupply[AssetId._LP_ASSET_ID], + initialSharePrice, + positionDuration, + timeStretch + ); + + // Update the amount of longs that have matured. + longsMatured += _bondAmount; + + // TODO: We could have a helper function for this. + // + // Since longs are backdated to the beginning of the checkpoint and + // interest only begins accruing when the longs are opened, we + // exclude the first checkpoint from LP withdrawal payouts. For most + // pools the difference will not be meaningful, and in edge cases, + // fees can be tuned to offset the problem. + uint256 openSharePrice = checkpoints[ + (_maturityTime - positionDuration) + checkpointDuration + ]; + // Apply the LP proceeds from the trade proportionally to the long // withdrawal shares. The accounting for these proceeds is identical // to the close short accounting because LPs take the short position - // when longs are opened. The math for the withdrawal proceeds is given - // by: + // when longs are opened; however, we can make some simplifications + // since the longs have matured. The math for the withdrawal proceeds is + // given by: // - // proceeds = c * (dy / c_0 - dz) * (min(b_x, dy) / dy) + // proceeds = c * (dy / c_0 - dy / c) * (min(b_x, dy) / dy) + // = (c / c_0 - 1) * dy * (min(b_x, dy) / dy) + // = (c / c_0 - 1) * min(b_x, dy) uint256 withdrawalAmount = longWithdrawalSharesOutstanding < _bondAmount ? longWithdrawalSharesOutstanding : _bondAmount; - uint256 withdrawalProceeds = _sharePrice - .mulDown(_bondAmount.divDown(_openSharePrice).sub(_shareProceeds)) - .mulDown(withdrawalAmount.divDown(_bondAmount)); + uint256 withdrawalProceeds = ( + _sharePrice.divDown(openSharePrice).sub(FixedPointMath.ONE_18) + ).mulDown(withdrawalAmount); longWithdrawalSharesOutstanding -= withdrawalAmount; longWithdrawalShareProceeds += withdrawalProceeds; - // Apply the trading deltas to the reserves. These updates reflect + // Apply the withdrawal payouts to the reserves. These updates reflect // the fact that some of the reserves will be attributed to the // withdrawal pool. The math for the share reserves update is given by: // - // z -= dz + (dy / c_0 - dz) * (min(b_x, dy) / dy) - shareReserves -= _shareProceeds.add( - withdrawalProceeds.divDown(_sharePrice) - ); + // z -= (c / c_0 - 1) * min(b_x, dy) / c + // = (1 / c_0 - 1 / c_1) * min(b_x, dy) + shareReserves -= withdrawalProceeds.divDown(_sharePrice); bondReserves = HyperdriveMath.calculateBondReserves( shareReserves, totalSupply[AssetId._LP_ASSET_ID], @@ -706,6 +796,9 @@ contract Hyperdrive is MultiToken { timeStretch ); + // Decrease the amount of shorts outstanding. + shortsOutstanding -= _bondAmount; + // Apply the LP proceeds from the trade proportionally to the short // withdrawal pool. The accounting for these proceeds is identical // to the close long accounting because LPs take on a long position when @@ -741,8 +834,6 @@ contract Hyperdrive is MultiToken { ); } - // TODO: We need to pay out the withdrawal pools in this function. - // /// @dev Creates a new checkpoint if necessary. /// @param _sharePrice The current share price. /// @return latestCheckpoint The latest checkpoint time. @@ -760,99 +851,33 @@ contract Hyperdrive is MultiToken { } // Pay out the long withdrawal pool for longs that have matured. - uint256 newlyMaturedLongs = totalSupply[ + uint256 maturedLongsAmount = totalSupply[ AssetId.encodeAssetId( AssetId.AssetIdPrefix.Long, 0, latestCheckpoint ) ]; - if (newlyMaturedLongs > 0) { - // Since longs are backdated to the beginning of the checkpoint and - // interest only begins accruing when the longs are opened, we - // exclude the first checkpoint from LP withdrawal payouts. For most - // pools the difference will not be meaningful, and in edge cases, - // fees can be tuned to offset the problem. - uint256 openSharePrice = checkpoints[ - (latestCheckpoint - positionDuration) + checkpointDuration - ]; + if (maturedLongsAmount > 0) { _applyMaturedLongsPayout( - newlyMaturedLongs, + maturedLongsAmount, _sharePrice, - openSharePrice + latestCheckpoint ); } // Pay out the short withdrawal pool for shorts that have matured. - if ( - totalSupply[ - AssetId.encodeAssetId( - AssetId.AssetIdPrefix.Short, - 0, - latestCheckpoint - ) - ] > 0 - ) { + uint256 maturedShortsAmount = totalSupply[ + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + 0, + latestCheckpoint + ) + ]; + if (maturedShortsAmount > 0) { // FIXME } return (latestCheckpoint, checkpoints[latestCheckpoint]); } - - // FIXME: Update longsMatured here and elsewhere. - // - /// @dev Pays out the profits from matured longs to the withdrawal pool. - /// @param _bondAmount The amount of longs that have matured. - /// @param _sharePrice The current share price. - /// @param _openSharePrice The share price when the longs were opened. - function _applyMaturedLongsPayout( - uint256 _bondAmount, - uint256 _sharePrice, - uint256 _openSharePrice - ) internal { - // Calculate the current pool APR. - uint256 apr = HyperdriveMath.calculateAPRFromReserves( - shareReserves, - bondReserves, - totalSupply[AssetId._LP_ASSET_ID], - initialSharePrice, - positionDuration, - timeStretch - ); - - // Apply the LP proceeds from the trade proportionally to the long - // withdrawal shares. The accounting for these proceeds is identical - // to the close short accounting because LPs take the short position - // when longs are opened; however, we can make some simplifications - // since the longs have matured. The math for the withdrawal proceeds is - // given by: - // - // proceeds = c * (dy / c_0 - dy / c) * (min(b_x, dy) / dy) - // = (c / c_0 - 1) * dy * (min(b_x, dy) / dy) - // = (c / c_0 - 1) * min(b_x, dy) - uint256 withdrawalAmount = longWithdrawalSharesOutstanding < _bondAmount - ? longWithdrawalSharesOutstanding - : _bondAmount; - uint256 withdrawalProceeds = ( - _sharePrice.divDown(_openSharePrice).sub(FixedPointMath.ONE_18) - ).mulDown(withdrawalAmount); - longWithdrawalSharesOutstanding -= withdrawalAmount; - longWithdrawalShareProceeds += withdrawalProceeds; - - // Apply the withdrawal payouts to the reserves. These updates reflect - // the fact that some of the reserves will be attributed to the - // withdrawal pool. The math for the share reserves update is given by: - // - // z -= (c / c_0 - 1) * min(b_x, dy) / c - // = (1 / c_0 - 1 / c_1) * min(b_x, dy) - shareReserves -= withdrawalProceeds.divDown(_sharePrice); - bondReserves = HyperdriveMath.calculateBondReserves( - shareReserves, - totalSupply[AssetId._LP_ASSET_ID], - initialSharePrice, - apr, - positionDuration, - timeStretch - ); - } } diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index ba509c0cb..f4cc57402 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -279,8 +279,10 @@ library HyperdriveMath { /// @param _shares The amount of LP shares burned from 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 _longsOutstanding The amount of longs that haven't been closed. + /// @param _longsMatured The amount of outstanding longs that have matured. + /// @param _shortsOutstanding The amount of shorts that haven't been closed. + /// @param _shortsMatured The amount of outstanding shorts that have matured. /// @param _sharePrice The pool's share price. /// @return shares The amount of base shares released. /// @return longWithdrawalShares The amount of long withdrawal shares @@ -292,7 +294,9 @@ library HyperdriveMath { uint256 _shareReserves, uint256 _lpTotalSupply, uint256 _longsOutstanding, + uint256 _longsMatured, uint256 _shortsOutstanding, + uint256 _shortsMatured, uint256 _sharePrice ) internal @@ -309,10 +313,13 @@ library HyperdriveMath { shares = _shareReserves .sub(_longsOutstanding.divDown(_sharePrice)) .mulDown(poolFactor); - // longsOutstanding * (dl / l) - longWithdrawalShares = _longsOutstanding.mulDown(poolFactor); - // shortsOutstanding * (dl / l) - shortWithdrawalShares = _shortsOutstanding.mulDown(poolFactor); + // (longsOutstanding - longsMatured) * (dl / l) + longWithdrawalShares = (_longsOutstanding.sub(_longsMatured)).mulDown( + poolFactor + ); + // (shortsOutstanding - shortsMatured) * (dl / l) + shortWithdrawalShares = (_shortsOutstanding.sub(_shortsMatured)) + .mulDown(poolFactor); return (shares, longWithdrawalShares, shortWithdrawalShares); } } From a1ae3398b638bb1b7e53092e5165ab37e5d0991a Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 10 Feb 2023 21:51:09 -0600 Subject: [PATCH 06/31] Updated the close short accounting for matured shorts --- contracts/Hyperdrive.sol | 281 +++++++++++++++++++++++---------------- 1 file changed, 168 insertions(+), 113 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 6e37f3af8..942c87293 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -419,24 +419,15 @@ contract Hyperdrive is MultiToken { false ); - // If there are outstanding long withdrawal shares, we attribute a - // proportional amount of the proceeds to the withdrawal pool and the - // active LPs. Otherwise, we use simplified accounting that has the same - // behavior but is more gas efficient. Since the difference between the - // base reserves and the longs outstanding stays the same or gets - // larger, we don't need to verify the reserves invariants. - if (longWithdrawalSharesOutstanding > 0) { - _applyCloseLong( - _bondAmount, - poolBondDelta, - shareProceeds, - sharePrice, - _maturityTime - ); - } else { - shareReserves -= shareProceeds; - bondReserves += poolBondDelta; - } + // Apply the accounting updates that result from closing the long to the + // reserves and pay out the withdrawal pool if necessary. + _applyCloseLong( + _bondAmount, + poolBondDelta, + shareProceeds, + sharePrice, + _maturityTime + ); // Withdraw the profit to the trader. // TODO: Better destination support. @@ -550,11 +541,8 @@ contract Hyperdrive is MultiToken { positionDuration ) // use divDown to scale to fixed point : 0; - ( - uint256 poolShareDelta, - uint256 poolBondDelta, - uint256 sharePayment - ) = HyperdriveMath.calculateSharesInGivenBondsOut( + (, uint256 poolBondDelta, uint256 sharePayment) = HyperdriveMath + .calculateSharesInGivenBondsOut( shareReserves, bondReserves, totalSupply[AssetId._LP_ASSET_ID], @@ -565,30 +553,16 @@ contract Hyperdrive is MultiToken { initialSharePrice ); - // FIXME: This behavior may need to be revised when we update the - // automatic closing flow. - // - // If there are outstanding short withdrawal shares, we attribute a - // proportional amount of the proceeds to the withdrawal pool and the - // active LPs. Otherwise, we use simplified accounting that has the same - // behavior but is more gas efficient. Since the share reserves increase - // or stay the same, there is no need to check that the share reserves - // are greater than or equal to the base buffer. - if (shortWithdrawalSharesOutstanding > 0) { - _applyCloseShort( - _bondAmount, - poolBondDelta, - sharePayment, - sharePrice - ); - } else { - shareReserves += poolShareDelta; - bondReserves -= poolBondDelta; - } + // Apply the accounting updates that result from closing the short to + // the reserves and pay out the withdrawal pool if necessary. + _applyCloseShort( + _bondAmount, + poolBondDelta, + sharePayment, + sharePrice, + _maturityTime + ); - // FIXME: We'll need to use a different share price if the short has - // already matured. - // // TODO: Double check this math. // // Withdraw the profit to the trader. This includes the proceeds from @@ -596,17 +570,21 @@ contract Hyperdrive is MultiToken { // on the face value of the bonds. The math for the short's proceeds in // base is given by: // - // proceeds = dy - c * dz + (c - c_0) * (dy / c_0) - // = dy - c * dz + (c / c_0) * dy - dy - // = (c / c_0) * dy - c * dz - // = c * (dy / c_0 - dz) + // proceeds = dy - c_1 * dz + (c_1 - c_0) * (dy / c_0) + // = dy - c_1 * dz + (c_1 / c_0) * dy - dy + // = (c_1 / c_0) * dy - c_1 * dz + // = c_1 * (dy / c_0 - dz) // // To convert to proceeds in shares, we simply divide by the current // share price. uint256 openSharePrice = checkpoints[_maturityTime - positionDuration]; - uint256 shortProceeds = _bondAmount.divDown(openSharePrice).sub( - sharePayment - ); + uint256 closeSharePrice = sharePrice; + if (_maturityTime <= block.timestamp) { + closeSharePrice = checkpoints[_maturityTime]; + } + uint256 shortProceeds = closeSharePrice + .mulDown(_bondAmount.divDown(openSharePrice).sub(sharePayment)) + .divDown(sharePrice); // TODO - Better destination support withdraw(shortProceeds, msg.sender); } @@ -630,24 +608,30 @@ contract Hyperdrive is MultiToken { uint256 _sharePrice, uint256 _maturityTime ) internal { - // Calculate the effect that the trade has on the pool's APR. - uint256 apr = HyperdriveMath.calculateAPRFromReserves( - shareReserves.sub(_shareProceeds), - bondReserves.add(_poolBondDelta), - totalSupply[AssetId._LP_ASSET_ID], - initialSharePrice, - positionDuration, - timeStretch - ); - - // Decrease the amount of longs outstanding. + // Reduce the amount of outstanding longs. longsOutstanding -= _bondAmount; - // If the bonds have already matured, then the withdrawal proceeds have - // already been accounted for. Otherwise, we calculate the withdrawal - // proceeds and pay out the withdrawal pool. - uint256 withdrawalProceeds = 0; - if (_maturityTime > block.timestamp) { + // If there are outstanding long withdrawal shares and the bonds haven't + // matured yet, we attribute a proportional amount of the proceeds to + // the withdrawal pool and the active LPs. Otherwise, we use simplified + // accounting that has the same behavior but is more gas efficient. + // Since the difference between the base reserves and the longs + // outstanding stays the same or gets larger, we don't need to verify + // the reserves invariants. + if ( + longWithdrawalSharesOutstanding > 0 && + _maturityTime > block.timestamp + ) { + // Calculate the effect that the trade has on the pool's APR. + uint256 apr = HyperdriveMath.calculateAPRFromReserves( + shareReserves.sub(_shareProceeds), + bondReserves.add(_poolBondDelta), + totalSupply[AssetId._LP_ASSET_ID], + initialSharePrice, + positionDuration, + timeStretch + ); + // Since longs are backdated to the beginning of the checkpoint and // interest only begins accruing when the longs are opened, we // exclude the first checkpoint from LP withdrawal payouts. For most @@ -668,7 +652,7 @@ contract Hyperdrive is MultiToken { _bondAmount ? longWithdrawalSharesOutstanding : _bondAmount; - withdrawalProceeds = _sharePrice + uint256 withdrawalProceeds = _sharePrice .mulDown( _bondAmount.divDown(openSharePrice).sub(_shareProceeds) ) @@ -677,30 +661,31 @@ contract Hyperdrive is MultiToken { // Update the long aggregates. longWithdrawalSharesOutstanding -= withdrawalAmount; longWithdrawalShareProceeds += withdrawalProceeds; + + // Apply the trading deltas to the reserves. These updates reflect + // the fact that some of the reserves will be attributed to the + // withdrawal pool. Assuming that there are some withdrawal proceeds, + // the math for the share reserves update is given by: + // + // z -= dz + (dy / c_0 - dz) * (min(b_x, dy) / dy) + shareReserves -= _shareProceeds.add( + withdrawalProceeds.divDown(_sharePrice) + ); + bondReserves = HyperdriveMath.calculateBondReserves( + shareReserves, + totalSupply[AssetId._LP_ASSET_ID], + initialSharePrice, + apr, + positionDuration, + timeStretch + ); } else { - // Since the bonds have already matured, we've already paid out the - // withdrawal proceeds. All that needs to be done is to update the - // amount longs matured. - longsMatured -= _bondAmount; + if (_maturityTime <= block.timestamp) { + longsMatured -= _bondAmount; + } + shareReserves -= _shareProceeds; + bondReserves += _poolBondDelta; } - - // Apply the trading deltas to the reserves. These updates reflect - // the fact that some of the reserves will be attributed to the - // withdrawal pool. Assuming that there are some withdrawal proceeds, - // the math for the share reserves update is given by: - // - // z -= dz + (dy / c_0 - dz) * (min(b_x, dy) / dy) - shareReserves -= _shareProceeds.add( - withdrawalProceeds.divDown(_sharePrice) - ); - bondReserves = HyperdriveMath.calculateBondReserves( - shareReserves, - totalSupply[AssetId._LP_ASSET_ID], - initialSharePrice, - apr, - positionDuration, - timeStretch - ); } /// @dev Pays out the profits from matured longs to the withdrawal pool. @@ -722,7 +707,7 @@ contract Hyperdrive is MultiToken { timeStretch ); - // Update the amount of longs that have matured. + // Increase the amount of longs that have matured. longsMatured += _bondAmount; // TODO: We could have a helper function for this. @@ -760,7 +745,6 @@ contract Hyperdrive is MultiToken { // withdrawal pool. The math for the share reserves update is given by: // // z -= (c / c_0 - 1) * min(b_x, dy) / c - // = (1 / c_0 - 1 / c_1) * min(b_x, dy) shareReserves -= withdrawalProceeds.divDown(_sharePrice); bondReserves = HyperdriveMath.calculateBondReserves( shareReserves, @@ -779,51 +763,120 @@ contract Hyperdrive is MultiToken { /// decreased by if we didn't need to account for the withdrawal /// pool. /// @param _sharePayment The payment in shares required to close the short. - /// @param _sharePrice The current share price + /// @param _sharePrice The current share price. + /// @param _maturityTime The maturity time of the bonds. function _applyCloseShort( uint256 _bondAmount, uint256 _poolBondDelta, uint256 _sharePayment, - uint256 _sharePrice + uint256 _sharePrice, + uint256 _maturityTime ) internal { + // Decrease the amount of shorts outstanding. + shortsOutstanding -= _bondAmount; + + // If there are outstanding short withdrawal shares and the bonds haven't + // matured yet, we attribute a proportional amount of the proceeds to + // the withdrawal pool and the active LPs. Otherwise, we use simplified + // accounting that has the same behavior but is more gas efficient. + // Since the difference between the base reserves and the longs + // outstanding stays the same or gets larger, we don't need to verify + // the reserves invariants. + if ( + shortWithdrawalSharesOutstanding > 0 && + _maturityTime > block.timestamp + ) { + // Calculate the effect that the trade has on the pool's APR. + uint256 apr = HyperdriveMath.calculateAPRFromReserves( + shareReserves.add(_sharePayment), + bondReserves.sub(_poolBondDelta), + totalSupply[AssetId._LP_ASSET_ID], + initialSharePrice, + positionDuration, + timeStretch + ); + + // Apply the LP proceeds from the trade proportionally to the short + // withdrawal pool. The accounting for these proceeds is identical + // to the close long accounting because LPs take on a long position when + // shorts are opened. The math for the withdrawal proceeds is given + // by: + // + // proceeds = c * dz * (min(b_y, dy) / dy) + uint256 withdrawalAmount = shortWithdrawalSharesOutstanding < + _bondAmount + ? shortWithdrawalSharesOutstanding + : _bondAmount; + uint256 withdrawalProceeds = _sharePrice + .mulDown(_sharePayment) + .mulDown(withdrawalAmount.divDown(_bondAmount)); + shortWithdrawalSharesOutstanding -= withdrawalAmount; + shortWithdrawalShareProceeds += withdrawalProceeds; + + // Apply the trading deltas to the reserves. These updates reflect + // the fact that some of the reserves will be attributed to the + // withdrawal pool. The math for the share reserves update is given by: + // + // z += dz - dz * (min(b_y, dy) / dy) + shareReserves += _sharePayment.sub( + withdrawalProceeds.divDown(_sharePrice) + ); + bondReserves = HyperdriveMath.calculateBondReserves( + shareReserves, + totalSupply[AssetId._LP_ASSET_ID], + initialSharePrice, + apr, + positionDuration, + timeStretch + ); + } else { + if (_maturityTime <= block.timestamp) { + shortsMatured -= _bondAmount; + } + shareReserves += _sharePayment; + bondReserves -= _poolBondDelta; + } + } + + /// @dev Pays out the profits from matured shorts to the withdrawal pool. + /// @param _bondAmount The amount of longs that have matured. + function _applyMaturedShortsPayout(uint256 _bondAmount) internal { // Calculate the effect that the trade has on the pool's APR. uint256 apr = HyperdriveMath.calculateAPRFromReserves( - shareReserves.add(_sharePayment), - bondReserves.sub(_poolBondDelta), + shareReserves, + bondReserves, totalSupply[AssetId._LP_ASSET_ID], initialSharePrice, positionDuration, timeStretch ); - // Decrease the amount of shorts outstanding. - shortsOutstanding -= _bondAmount; + // Increase the amount of shorts that have matured. + shortsMatured += _bondAmount; // Apply the LP proceeds from the trade proportionally to the short // withdrawal pool. The accounting for these proceeds is identical - // to the close long accounting because LPs take on a long position when - // shorts are opened. The math for the withdrawal proceeds is given - // by: + // to the close short accounting because LPs take on a long position when + // shorts are opened; however, we can make some simplifications + // since the longs have matured. The math for the withdrawal proceeds is + // given by: // - // proceeds = c * dz * (min(b_y, dy) / dy) + // proceeds = c * (dy / c) * (min(b_y, dy) / dy) + // = dy * (min(b_y, dy) / dy) + // = min(b_y, dy) uint256 withdrawalAmount = shortWithdrawalSharesOutstanding < _bondAmount ? shortWithdrawalSharesOutstanding : _bondAmount; - uint256 withdrawalProceeds = _sharePrice.mulDown(_sharePayment).mulDown( - withdrawalAmount.divDown(_bondAmount) - ); shortWithdrawalSharesOutstanding -= withdrawalAmount; - shortWithdrawalShareProceeds += withdrawalProceeds; + shortWithdrawalShareProceeds += withdrawalAmount; // Apply the trading deltas to the reserves. These updates reflect // the fact that some of the reserves will be attributed to the // withdrawal pool. The math for the share reserves update is given by: // - // z += dz - dz * (min(b_y, dy) / dy) - shareReserves += _sharePayment.sub( - withdrawalProceeds.divDown(_sharePrice) - ); + // z -= min(b_y, dy) + shareReserves -= withdrawalAmount; bondReserves = HyperdriveMath.calculateBondReserves( shareReserves, totalSupply[AssetId._LP_ASSET_ID], @@ -834,6 +887,8 @@ contract Hyperdrive is MultiToken { ); } + // TODO: Create a publicly accessible checkpoint flow. + // /// @dev Creates a new checkpoint if necessary. /// @param _sharePrice The current share price. /// @return latestCheckpoint The latest checkpoint time. @@ -875,7 +930,7 @@ contract Hyperdrive is MultiToken { ) ]; if (maturedShortsAmount > 0) { - // FIXME + _applyMaturedShortsPayout(maturedShortsAmount); } return (latestCheckpoint, checkpoints[latestCheckpoint]); From fed7fa51910b399b94eb941bca26cdda07b8b12a Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 10 Feb 2023 22:03:11 -0600 Subject: [PATCH 07/31] Adds a naive external checkpointing flow --- contracts/Hyperdrive.sol | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 942c87293..7c19d1e63 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -43,8 +43,6 @@ contract Hyperdrive is MultiToken { // @dev The share price at the time the pool was created. uint256 public immutable initialSharePrice; - // TODO: We'll likely need to add more information to these checkpoints. - // /// @dev Checkpoints of historical share prices. mapping(uint256 => uint256) public checkpoints; @@ -589,6 +587,13 @@ contract Hyperdrive is MultiToken { withdraw(shortProceeds, msg.sender); } + /// Checkpoint /// + + /// @notice Allows anyone to mint a new checkpoint. + function checkpoint() external { + _checkpoint(pricePerShare()); + } + /// Helpers /// /// @dev Applies the trading deltas from a closed long to the reserves and @@ -887,8 +892,6 @@ contract Hyperdrive is MultiToken { ); } - // TODO: Create a publicly accessible checkpoint flow. - // /// @dev Creates a new checkpoint if necessary. /// @param _sharePrice The current share price. /// @return latestCheckpoint The latest checkpoint time. From d45a73115c93f8b29ec5b87cfa5d5a19701bab1a Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 10 Feb 2023 22:20:52 -0600 Subject: [PATCH 08/31] Made the external checkpointing flow more robust --- contracts/Hyperdrive.sol | 78 +++++++++++++++++++++++++--------- contracts/libraries/Errors.sol | 1 + 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 7c19d1e63..4dedc3406 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -159,7 +159,9 @@ contract Hyperdrive is MultiToken { (uint256 shares, uint256 sharePrice) = deposit(_contribution); // Create an initial checkpoint. - _checkpoint(sharePrice); + uint256 latestCheckpoint = block.timestamp - + (block.timestamp % checkpointDuration); + _checkpoint(latestCheckpoint, sharePrice); // Update the reserves. The bond reserves are calculated so that the // pool is initialized with the target APR. @@ -321,7 +323,9 @@ contract Hyperdrive is MultiToken { (uint256 shares, uint256 sharePrice) = deposit(_baseAmount); // Perform a checkpoint. - (uint256 latestCheckpoint, ) = _checkpoint(sharePrice); + uint256 latestCheckpoint = block.timestamp - + (block.timestamp % checkpointDuration); + _checkpoint(latestCheckpoint, sharePrice); // Calculate the pool and user deltas using the trading function. We // backdate the bonds purchased to the beginning of the checkpoint. We @@ -388,7 +392,9 @@ contract Hyperdrive is MultiToken { // Perform a checkpoint. uint256 sharePrice = pricePerShare(); - _checkpoint(sharePrice); + uint256 latestCheckpoint = block.timestamp - + (block.timestamp % checkpointDuration); + _checkpoint(latestCheckpoint, sharePrice); // Burn the longs that are being closed. uint256 assetId = AssetId.encodeAssetId( @@ -446,9 +452,9 @@ contract Hyperdrive is MultiToken { // Since the short will receive interest from the beginning of the // checkpoint, they will receive this backdated interest back at closing. uint256 sharePrice = pricePerShare(); - (uint256 latestCheckpoint, uint256 openSharePrice) = _checkpoint( - sharePrice - ); + uint256 latestCheckpoint = block.timestamp - + (block.timestamp % checkpointDuration); + uint256 openSharePrice = _checkpoint(latestCheckpoint, sharePrice); // Calculate the pool and user deltas using the trading function. We // backdate the bonds sold to the beginning of the checkpoint. @@ -523,7 +529,9 @@ contract Hyperdrive is MultiToken { // Perform a checkpoint. uint256 sharePrice = pricePerShare(); - _checkpoint(sharePrice); + uint256 latestCheckpoint = block.timestamp - + (block.timestamp % checkpointDuration); + _checkpoint(latestCheckpoint, sharePrice); // Burn the shorts that are being closed. uint256 assetId = AssetId.encodeAssetId( @@ -590,8 +598,38 @@ contract Hyperdrive is MultiToken { /// Checkpoint /// /// @notice Allows anyone to mint a new checkpoint. - function checkpoint() external { - _checkpoint(pricePerShare()); + /// @param _checkpointTime The time of the checkpoint to create. + function checkpoint(uint256 _checkpointTime) external { + // If the checkpoint has already been set, return early. + if (checkpoints[_checkpointTime] != 0) { + return; + } + + // If the checkpoint time isn't divisible by the checkpoint duration + // or is in the future, it's an invalid checkpoint and we should + // revert. + uint256 latestCheckpoint = block.timestamp - + (block.timestamp % checkpointDuration); + if ( + _checkpointTime % checkpointDuration == 0 || + latestCheckpoint < _checkpointTime + ) { + revert Errors.InvalidCheckpointTime(); + } + + // If the checkpoint time is the latest checkpoint, we use the current + // share price. Otherwise, we use a linear search to find the closest + // share price and use that to perform the checkpoint. + if (latestCheckpoint == _checkpointTime) { + _checkpoint(latestCheckpoint, pricePerShare()); + } else { + for (uint256 time = _checkpointTime; ; time += checkpointDuration) { + uint256 closestSharePrice = checkpoints[time]; + if (closestSharePrice != 0) { + _checkpoint(_checkpointTime, closestSharePrice); + } + } + } } /// Helpers /// @@ -893,19 +931,17 @@ contract Hyperdrive is MultiToken { } /// @dev Creates a new checkpoint if necessary. + /// @param _checkpointTime The time of the checkpoint to create. /// @param _sharePrice The current share price. - /// @return latestCheckpoint The latest checkpoint time. /// @return openSharePrice The open share price of the latest checkpoint. function _checkpoint( + uint256 _checkpointTime, uint256 _sharePrice - ) internal returns (uint256 latestCheckpoint, uint256 openSharePrice) { + ) internal returns (uint256 openSharePrice) { // Return early if the checkpoint has already been updated. - latestCheckpoint = - block.timestamp - - (block.timestamp % checkpointDuration); - if (checkpoints[latestCheckpoint] == 0) { - checkpoints[latestCheckpoint] = _sharePrice; - return (latestCheckpoint, _sharePrice); + if (checkpoints[_checkpointTime] == 0) { + checkpoints[_checkpointTime] = _sharePrice; + return _sharePrice; } // Pay out the long withdrawal pool for longs that have matured. @@ -913,14 +949,14 @@ contract Hyperdrive is MultiToken { AssetId.encodeAssetId( AssetId.AssetIdPrefix.Long, 0, - latestCheckpoint + _checkpointTime ) ]; if (maturedLongsAmount > 0) { _applyMaturedLongsPayout( maturedLongsAmount, _sharePrice, - latestCheckpoint + _checkpointTime ); } @@ -929,13 +965,13 @@ contract Hyperdrive is MultiToken { AssetId.encodeAssetId( AssetId.AssetIdPrefix.Short, 0, - latestCheckpoint + _checkpointTime ) ]; if (maturedShortsAmount > 0) { _applyMaturedShortsPayout(maturedShortsAmount); } - return (latestCheckpoint, checkpoints[latestCheckpoint]); + return checkpoints[_checkpointTime]; } } diff --git a/contracts/libraries/Errors.sol b/contracts/libraries/Errors.sol index 3836acc6e..b032e4129 100644 --- a/contracts/libraries/Errors.sol +++ b/contracts/libraries/Errors.sol @@ -12,6 +12,7 @@ library Errors { /// ### Hyperdrive ### /// ################## error BaseBufferExceedsShareReserves(); + error InvalidCheckpointTime(); error InvalidCheckpointDuration(); error InvalidMaturityTime(); error PoolAlreadyInitialized(); From e465e0f146eee8e0ee1939254cf679723d9a426c Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 10 Feb 2023 22:39:10 -0600 Subject: [PATCH 09/31] Removed extra data from the asset ID system --- contracts/Hyperdrive.sol | 52 ++++++++------------------------- contracts/libraries/AssetId.sol | 43 ++++++++------------------- 2 files changed, 24 insertions(+), 71 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 4dedc3406..faa4781d3 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -285,11 +285,7 @@ contract Hyperdrive is MultiToken { // // Mint the long and short withdrawal tokens. _mint( - AssetId.encodeAssetId( - AssetId.AssetIdPrefix.LongWithdrawalShare, - 0, - 0 - ), + AssetId.encodeAssetId(AssetId.AssetIdPrefix.LongWithdrawalShare, 0), msg.sender, longWithdrawalShares ); @@ -297,7 +293,6 @@ contract Hyperdrive is MultiToken { _mint( AssetId.encodeAssetId( AssetId.AssetIdPrefix.ShortWithdrawalShare, - 0, 0 ), msg.sender, @@ -367,25 +362,16 @@ contract Hyperdrive is MultiToken { // Mint the bonds to the trader with an ID of the maturity time. _mint( - AssetId.encodeAssetId( - AssetId.AssetIdPrefix.Long, - sharePrice, - maturityTime - ), + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), msg.sender, bondProceeds ); } /// @notice Closes a long position with a specified maturity time. - /// @param _openSharePrice The opening share price of the short. /// @param _maturityTime The maturity time of the short. /// @param _bondAmount The amount of longs to close. - function closeLong( - uint256 _openSharePrice, - uint32 _maturityTime, - uint256 _bondAmount - ) external { + function closeLong(uint32 _maturityTime, uint256 _bondAmount) external { if (_bondAmount == 0) { revert Errors.ZeroAmount(); } @@ -399,7 +385,6 @@ contract Hyperdrive is MultiToken { // Burn the longs that are being closed. uint256 assetId = AssetId.encodeAssetId( AssetId.AssetIdPrefix.Long, - _openSharePrice, _maturityTime ); _burn(assetId, msg.sender, _bondAmount); @@ -504,25 +489,16 @@ contract Hyperdrive is MultiToken { // Mint the short tokens to the trader. The ID is a concatenation of the // current share price and the maturity time of the shorts. _mint( - AssetId.encodeAssetId( - AssetId.AssetIdPrefix.Short, - sharePrice, - maturityTime - ), + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, maturityTime), msg.sender, _bondAmount ); } /// @notice Closes a short position with a specified maturity time. - /// @param _openSharePrice The opening share price of the short. /// @param _maturityTime The maturity time of the short. /// @param _bondAmount The amount of shorts to close. - function closeShort( - uint256 _openSharePrice, - uint32 _maturityTime, - uint256 _bondAmount - ) external { + function closeShort(uint32 _maturityTime, uint256 _bondAmount) external { if (_bondAmount == 0) { revert Errors.ZeroAmount(); } @@ -536,7 +512,6 @@ contract Hyperdrive is MultiToken { // Burn the shorts that are being closed. uint256 assetId = AssetId.encodeAssetId( AssetId.AssetIdPrefix.Short, - _openSharePrice, _maturityTime ); _burn(assetId, msg.sender, _bondAmount); @@ -930,6 +905,11 @@ contract Hyperdrive is MultiToken { ); } + // TODO: If we find that this checkpointing flow is too heavy (which is + // quite possible), we can store the share price and update some key metrics + // about matured positions and add a poking system that performs the rest of + // the computation. + // /// @dev Creates a new checkpoint if necessary. /// @param _checkpointTime The time of the checkpoint to create. /// @param _sharePrice The current share price. @@ -946,11 +926,7 @@ contract Hyperdrive is MultiToken { // Pay out the long withdrawal pool for longs that have matured. uint256 maturedLongsAmount = totalSupply[ - AssetId.encodeAssetId( - AssetId.AssetIdPrefix.Long, - 0, - _checkpointTime - ) + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, _checkpointTime) ]; if (maturedLongsAmount > 0) { _applyMaturedLongsPayout( @@ -962,11 +938,7 @@ contract Hyperdrive is MultiToken { // Pay out the short withdrawal pool for shorts that have matured. uint256 maturedShortsAmount = totalSupply[ - AssetId.encodeAssetId( - AssetId.AssetIdPrefix.Short, - 0, - _checkpointTime - ) + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, _checkpointTime) ]; if (maturedShortsAmount > 0) { _applyMaturedShortsPayout(maturedShortsAmount); diff --git a/contracts/libraries/AssetId.sol b/contracts/libraries/AssetId.sol index 52308f34b..08fc96c50 100644 --- a/contracts/libraries/AssetId.sol +++ b/contracts/libraries/AssetId.sol @@ -24,34 +24,20 @@ library AssetId { ShortWithdrawalShare } - /// @dev Encodes an identifier, data, and a timestamp into an asset ID. - /// Asset IDs are used so that LP, long, and short tokens can all be - /// represented in a single MultiToken instance. The zero asset ID - /// indicates the LP token. - /// TODO: Update this comment when we make the range more restrictive. + /// @dev Encodes a prefix and a timestamp into an asset ID. Asset IDs are + /// used so that LP, long, and short tokens can all be represented in a + /// single MultiToken instance. The zero asset ID indicates the LP + /// token. /// @param _prefix A one byte prefix that specifies the asset type. - /// @param _data Data associated with the asset. This is an efficient way of - /// fingerprinting data as the user can supply this data, and the - /// token balance ensures that the data is associated with the asset. /// @param _timestamp A timestamp associated with the asset. /// @return id The asset ID. function encodeAssetId( AssetIdPrefix _prefix, - uint256 _data, uint256 _timestamp ) internal pure returns (uint256 id) { - // Ensure that _data is a 216 bit number. - if (_data > 0xffffffffffffffffffffffffffffffffffffffffffffffffffffff) { - revert Errors.AssetIDCorruption(); - } - // [identifier: 8 bits][data: 216 bits][timestamp: 32 bits] + // [identifier: 8 bits][timestamp: 248 bits] assembly { - id := or( - or(shl(0xf8, _prefix), shl(0x20, _data)), - // ensure max timestamp is 0xffffffff - // valid until Sun Feb 07 2106 06:28:15 - mod(_timestamp, shl(0x21, 1)) - ) + id := or(shl(0xf8, _prefix), _timestamp) } return id; } @@ -60,22 +46,17 @@ library AssetId { /// identifier, data and a timestamp. /// @param _id The asset ID. /// @return _prefix A one byte prefix that specifies the asset type. - /// @return _data Data associated with the asset. This is an efficient way of - /// fingerprinting data as the user can supply this data, and the - /// token balance ensures that the data is associated with the asset. /// @return _timestamp A timestamp associated with the asset. function decodeAssetId( uint256 _id - ) - internal - pure - returns (AssetIdPrefix _prefix, uint256 _data, uint256 _timestamp) - { - // [identifier: 8 bits][data: 216 bits][timestamp: 32 bits] + ) internal pure returns (AssetIdPrefix _prefix, uint256 _timestamp) { + // [identifier: 8 bits][timestamp: 248 bits] assembly { _prefix := shr(0xf8, _id) // shr 248 bits - _data := shr(0x28, shl(0x8, _id)) // shl 8 bits, shr 40 bits - _timestamp := and(0xffffffff, _id) // 32 bit-mask + _timestamp := and( + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, + _id + ) // 248 bit-mask } } } From d1cf43e54dca5c3285ad51a4b4dc19eb4e382ad0 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 10 Feb 2023 22:52:57 -0600 Subject: [PATCH 10/31] Adds a redemption flow for the withdrawal shares --- contracts/Hyperdrive.sol | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index faa4781d3..e72143920 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -305,6 +305,57 @@ contract Hyperdrive is MultiToken { withdraw(shareProceeds, msg.sender); } + /// @notice Redeems long and short withdrawal shares. + /// @param _longWithdrawalShares The long withdrawal shares to redeem. + /// @param _shortWithdrawalShares The short withdrawal shares to redeem. + function redeemWithdrawalShares( + uint256 _longWithdrawalShares, + uint256 _shortWithdrawalShares + ) external { + uint256 baseProceeds = 0; + + // Redeem the long withdrawal shares. + if (_longWithdrawalShares > 0) { + // Burn the long withdrawal shares. + uint256 assetId = AssetId.encodeAssetId( + AssetId.AssetIdPrefix.LongWithdrawalShare, + 0 + ); + _burn(assetId, msg.sender, _longWithdrawalShares); + + // Calculate the base released from the withdrawal shares. + uint256 withdrawalShareProportion = _longWithdrawalShares.mulDown( + totalSupply[assetId].sub(longWithdrawalSharesOutstanding) + ); + baseProceeds += longWithdrawalShareProceeds.mulDown( + withdrawalShareProportion + ); + } + + // Redeem the short withdrawal shares. + if (_shortWithdrawalShares > 0) { + // Burn the short withdrawal shares. + uint256 assetId = AssetId.encodeAssetId( + AssetId.AssetIdPrefix.ShortWithdrawalShare, + 0 + ); + _burn(assetId, msg.sender, _longWithdrawalShares); + + // Calculate the base released from the withdrawal shares. + uint256 withdrawalShareProportion = _shortWithdrawalShares.mulDown( + totalSupply[assetId].sub(shortWithdrawalSharesOutstanding) + ); + baseProceeds += shortWithdrawalShareProceeds.mulDown( + withdrawalShareProportion + ); + } + + // Withdraw the funds released by redeeming the withdrawal shares. + // TODO: Better destination support. + uint256 shareProceeds = baseProceeds.divDown(pricePerShare()); + withdraw(shareProceeds, msg.sender); + } + /// Long /// /// @notice Opens a long position. From a320f188bae1f585783c67d4c71c07bb538624b9 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sat, 11 Feb 2023 20:35:57 -0600 Subject: [PATCH 11/31] Fixed a bug in the "_checkpoint" function --- .gitignore | 3 +++ contracts/Hyperdrive.sol | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 4bb3c375a..330bdaee0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ node_modules/ yarn-error.log .direnv + +# Vim +remappings.txt diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index e72143920..bbcce32f1 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -977,7 +977,10 @@ contract Hyperdrive is MultiToken { // Pay out the long withdrawal pool for longs that have matured. uint256 maturedLongsAmount = totalSupply[ - AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, _checkpointTime) + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + _checkpointTime - positionDuration + ) ]; if (maturedLongsAmount > 0) { _applyMaturedLongsPayout( @@ -989,7 +992,10 @@ contract Hyperdrive is MultiToken { // Pay out the short withdrawal pool for shorts that have matured. uint256 maturedShortsAmount = totalSupply[ - AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, _checkpointTime) + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + _checkpointTime - positionDuration + ) ]; if (maturedShortsAmount > 0) { _applyMaturedShortsPayout(maturedShortsAmount); From 487063526fc4800ab5ad6e0c602b6f3a0cea8fa0 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sun, 12 Feb 2023 13:06:10 -0600 Subject: [PATCH 12/31] Fixed a bug in the checkpointing flow --- contracts/Hyperdrive.sol | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index bbcce32f1..c9ff2eccc 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -971,16 +971,15 @@ contract Hyperdrive is MultiToken { ) internal returns (uint256 openSharePrice) { // Return early if the checkpoint has already been updated. if (checkpoints[_checkpointTime] == 0) { - checkpoints[_checkpointTime] = _sharePrice; - return _sharePrice; + return checkpoints[_checkpointTime]; } + // Create the share price checkpoint. + checkpoints[_checkpointTime] = _sharePrice; + // Pay out the long withdrawal pool for longs that have matured. uint256 maturedLongsAmount = totalSupply[ - AssetId.encodeAssetId( - AssetId.AssetIdPrefix.Long, - _checkpointTime - positionDuration - ) + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, _checkpointTime) ]; if (maturedLongsAmount > 0) { _applyMaturedLongsPayout( @@ -992,10 +991,7 @@ contract Hyperdrive is MultiToken { // Pay out the short withdrawal pool for shorts that have matured. uint256 maturedShortsAmount = totalSupply[ - AssetId.encodeAssetId( - AssetId.AssetIdPrefix.Short, - _checkpointTime - positionDuration - ) + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, _checkpointTime) ]; if (maturedShortsAmount > 0) { _applyMaturedShortsPayout(maturedShortsAmount); From d11364aa1d818de3498976771ad6260e04c8238e Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sun, 12 Feb 2023 15:23:52 -0600 Subject: [PATCH 13/31] Fixed some issues found during testing --- contracts/Hyperdrive.sol | 197 ++++++++++++++++++++------------------- 1 file changed, 101 insertions(+), 96 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index c9ff2eccc..6cf7ea8c3 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -637,7 +637,7 @@ contract Hyperdrive is MultiToken { uint256 latestCheckpoint = block.timestamp - (block.timestamp % checkpointDuration); if ( - _checkpointTime % checkpointDuration == 0 || + _checkpointTime % checkpointDuration != 0 || latestCheckpoint < _checkpointTime ) { revert Errors.InvalidCheckpointTime(); @@ -766,63 +766,66 @@ contract Hyperdrive is MultiToken { uint256 _sharePrice, uint256 _maturityTime ) internal { - // Calculate the current pool APR. - uint256 apr = HyperdriveMath.calculateAPRFromReserves( - shareReserves, - bondReserves, - totalSupply[AssetId._LP_ASSET_ID], - initialSharePrice, - positionDuration, - timeStretch - ); - // Increase the amount of longs that have matured. longsMatured += _bondAmount; - // TODO: We could have a helper function for this. - // - // Since longs are backdated to the beginning of the checkpoint and - // interest only begins accruing when the longs are opened, we - // exclude the first checkpoint from LP withdrawal payouts. For most - // pools the difference will not be meaningful, and in edge cases, - // fees can be tuned to offset the problem. - uint256 openSharePrice = checkpoints[ - (_maturityTime - positionDuration) + checkpointDuration - ]; + if (longWithdrawalSharesOutstanding > 0) { + // Calculate the current pool APR. + uint256 apr = HyperdriveMath.calculateAPRFromReserves( + shareReserves, + bondReserves, + totalSupply[AssetId._LP_ASSET_ID], + initialSharePrice, + positionDuration, + timeStretch + ); - // Apply the LP proceeds from the trade proportionally to the long - // withdrawal shares. The accounting for these proceeds is identical - // to the close short accounting because LPs take the short position - // when longs are opened; however, we can make some simplifications - // since the longs have matured. The math for the withdrawal proceeds is - // given by: - // - // proceeds = c * (dy / c_0 - dy / c) * (min(b_x, dy) / dy) - // = (c / c_0 - 1) * dy * (min(b_x, dy) / dy) - // = (c / c_0 - 1) * min(b_x, dy) - uint256 withdrawalAmount = longWithdrawalSharesOutstanding < _bondAmount - ? longWithdrawalSharesOutstanding - : _bondAmount; - uint256 withdrawalProceeds = ( - _sharePrice.divDown(openSharePrice).sub(FixedPointMath.ONE_18) - ).mulDown(withdrawalAmount); - longWithdrawalSharesOutstanding -= withdrawalAmount; - longWithdrawalShareProceeds += withdrawalProceeds; - - // Apply the withdrawal payouts to the reserves. These updates reflect - // the fact that some of the reserves will be attributed to the - // withdrawal pool. The math for the share reserves update is given by: - // - // z -= (c / c_0 - 1) * min(b_x, dy) / c - shareReserves -= withdrawalProceeds.divDown(_sharePrice); - bondReserves = HyperdriveMath.calculateBondReserves( - shareReserves, - totalSupply[AssetId._LP_ASSET_ID], - initialSharePrice, - apr, - positionDuration, - timeStretch - ); + // TODO: We could have a helper function for this. + // + // Since longs are backdated to the beginning of the checkpoint and + // interest only begins accruing when the longs are opened, we + // exclude the first checkpoint from LP withdrawal payouts. For most + // pools the difference will not be meaningful, and in edge cases, + // fees can be tuned to offset the problem. + uint256 openSharePrice = checkpoints[ + (_maturityTime - positionDuration) + checkpointDuration + ]; + + // Apply the LP proceeds from the trade proportionally to the long + // withdrawal shares. The accounting for these proceeds is identical + // to the close short accounting because LPs take the short position + // when longs are opened; however, we can make some simplifications + // since the longs have matured. The math for the withdrawal proceeds is + // given by: + // + // proceeds = c * (dy / c_0 - dy / c) * (min(b_x, dy) / dy) + // = (c / c_0 - 1) * dy * (min(b_x, dy) / dy) + // = (c / c_0 - 1) * min(b_x, dy) + uint256 withdrawalAmount = longWithdrawalSharesOutstanding < + _bondAmount + ? longWithdrawalSharesOutstanding + : _bondAmount; + uint256 withdrawalProceeds = ( + _sharePrice.divDown(openSharePrice).sub(FixedPointMath.ONE_18) + ).mulDown(withdrawalAmount); + longWithdrawalSharesOutstanding -= withdrawalAmount; + longWithdrawalShareProceeds += withdrawalProceeds; + + // Apply the withdrawal payouts to the reserves. These updates reflect + // the fact that some of the reserves will be attributed to the + // withdrawal pool. The math for the share reserves update is given by: + // + // z -= (c / c_0 - 1) * min(b_x, dy) / c + shareReserves -= withdrawalProceeds.divDown(_sharePrice); + bondReserves = HyperdriveMath.calculateBondReserves( + shareReserves, + totalSupply[AssetId._LP_ASSET_ID], + initialSharePrice, + apr, + positionDuration, + timeStretch + ); + } } /// @dev Applies the trading deltas from a closed short to the reserves and @@ -910,50 +913,52 @@ contract Hyperdrive is MultiToken { /// @dev Pays out the profits from matured shorts to the withdrawal pool. /// @param _bondAmount The amount of longs that have matured. function _applyMaturedShortsPayout(uint256 _bondAmount) internal { - // Calculate the effect that the trade has on the pool's APR. - uint256 apr = HyperdriveMath.calculateAPRFromReserves( - shareReserves, - bondReserves, - totalSupply[AssetId._LP_ASSET_ID], - initialSharePrice, - positionDuration, - timeStretch - ); - // Increase the amount of shorts that have matured. shortsMatured += _bondAmount; - // Apply the LP proceeds from the trade proportionally to the short - // withdrawal pool. The accounting for these proceeds is identical - // to the close short accounting because LPs take on a long position when - // shorts are opened; however, we can make some simplifications - // since the longs have matured. The math for the withdrawal proceeds is - // given by: - // - // proceeds = c * (dy / c) * (min(b_y, dy) / dy) - // = dy * (min(b_y, dy) / dy) - // = min(b_y, dy) - uint256 withdrawalAmount = shortWithdrawalSharesOutstanding < - _bondAmount - ? shortWithdrawalSharesOutstanding - : _bondAmount; - shortWithdrawalSharesOutstanding -= withdrawalAmount; - shortWithdrawalShareProceeds += withdrawalAmount; - - // Apply the trading deltas to the reserves. These updates reflect - // the fact that some of the reserves will be attributed to the - // withdrawal pool. The math for the share reserves update is given by: - // - // z -= min(b_y, dy) - shareReserves -= withdrawalAmount; - bondReserves = HyperdriveMath.calculateBondReserves( - shareReserves, - totalSupply[AssetId._LP_ASSET_ID], - initialSharePrice, - apr, - positionDuration, - timeStretch - ); + if (shortWithdrawalSharesOutstanding > 0) { + // Calculate the effect that the trade has on the pool's APR. + uint256 apr = HyperdriveMath.calculateAPRFromReserves( + shareReserves, + bondReserves, + totalSupply[AssetId._LP_ASSET_ID], + initialSharePrice, + positionDuration, + timeStretch + ); + + // Apply the LP proceeds from the trade proportionally to the short + // withdrawal pool. The accounting for these proceeds is identical + // to the close short accounting because LPs take on a long position when + // shorts are opened; however, we can make some simplifications + // since the longs have matured. The math for the withdrawal proceeds is + // given by: + // + // proceeds = c * (dy / c) * (min(b_y, dy) / dy) + // = dy * (min(b_y, dy) / dy) + // = min(b_y, dy) + uint256 withdrawalAmount = shortWithdrawalSharesOutstanding < + _bondAmount + ? shortWithdrawalSharesOutstanding + : _bondAmount; + shortWithdrawalSharesOutstanding -= withdrawalAmount; + shortWithdrawalShareProceeds += withdrawalAmount; + + // Apply the trading deltas to the reserves. These updates reflect + // the fact that some of the reserves will be attributed to the + // withdrawal pool. The math for the share reserves update is given by: + // + // z -= min(b_y, dy) + shareReserves -= withdrawalAmount; + bondReserves = HyperdriveMath.calculateBondReserves( + shareReserves, + totalSupply[AssetId._LP_ASSET_ID], + initialSharePrice, + apr, + positionDuration, + timeStretch + ); + } } // TODO: If we find that this checkpointing flow is too heavy (which is @@ -970,7 +975,7 @@ contract Hyperdrive is MultiToken { uint256 _sharePrice ) internal returns (uint256 openSharePrice) { // Return early if the checkpoint has already been updated. - if (checkpoints[_checkpointTime] == 0) { + if (checkpoints[_checkpointTime] != 0) { return checkpoints[_checkpointTime]; } From 9e31428751bdccfd224f8a6cbbb8546a6ea8ed18 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sun, 12 Feb 2023 20:50:59 -0600 Subject: [PATCH 14/31] Simplify the matured positions accounting --- contracts/Hyperdrive.sol | 240 ++++++------------------- contracts/libraries/HyperdriveMath.sol | 15 +- 2 files changed, 59 insertions(+), 196 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 6cf7ea8c3..22ee24fef 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -15,10 +15,6 @@ import { MultiToken } from "contracts/MultiToken.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -// -// TODO - Here we give default implementations of the virtual methods to not break tests -// we should move to an abstract contract to prevent this from being deployed w/o -// real implementations. contract Hyperdrive is MultiToken { using FixedPointMath for uint256; @@ -63,12 +59,6 @@ contract Hyperdrive is MultiToken { /// @notice The amount of shorts that are still open. uint256 public shortsOutstanding; - /// @notice The amount of longs that have matured but have not been closed. - uint256 public longsMatured; - - /// @notice The amount of shorts that have matured but have not been closed. - uint256 public shortsMatured; - /// @notice The amount of long withdrawal shares that haven't been paid out. uint256 public longWithdrawalSharesOutstanding; @@ -261,9 +251,7 @@ contract Hyperdrive is MultiToken { shareReserves, totalSupply[AssetId._LP_ASSET_ID], longsOutstanding, - longsMatured, shortsOutstanding, - shortsMatured, pricePerShare() ); @@ -459,15 +447,19 @@ contract Hyperdrive is MultiToken { false ); - // Apply the accounting updates that result from closing the long to the - // reserves and pay out the withdrawal pool if necessary. - _applyCloseLong( - _bondAmount, - poolBondDelta, - shareProceeds, - sharePrice, - _maturityTime - ); + // If the position hasn't matured, apply the accounting updates that + // result from closing the long to the reserves and pay out the + // withdrawal pool if necessary. Matured positions have already been + // accounted for. + if (block.timestamp < _maturityTime) { + _applyCloseLong( + _bondAmount, + poolBondDelta, + shareProceeds, + sharePrice, + _maturityTime + ); + } // Withdraw the profit to the trader. // TODO: Better destination support. @@ -585,15 +577,18 @@ contract Hyperdrive is MultiToken { initialSharePrice ); - // Apply the accounting updates that result from closing the short to - // the reserves and pay out the withdrawal pool if necessary. - _applyCloseShort( - _bondAmount, - poolBondDelta, - sharePayment, - sharePrice, - _maturityTime - ); + // If the position hasn't matured, apply the accounting updates that + // result from closing the short to the reserves and pay out the + // withdrawal pool if necessary. Matured positions have already been + // accounted for. + if (block.timestamp < _maturityTime) { + _applyCloseShort( + _bondAmount, + poolBondDelta, + sharePayment, + sharePrice + ); + } // TODO: Double check this math. // @@ -680,17 +675,13 @@ contract Hyperdrive is MultiToken { // Reduce the amount of outstanding longs. longsOutstanding -= _bondAmount; - // If there are outstanding long withdrawal shares and the bonds haven't - // matured yet, we attribute a proportional amount of the proceeds to - // the withdrawal pool and the active LPs. Otherwise, we use simplified - // accounting that has the same behavior but is more gas efficient. - // Since the difference between the base reserves and the longs - // outstanding stays the same or gets larger, we don't need to verify - // the reserves invariants. - if ( - longWithdrawalSharesOutstanding > 0 && - _maturityTime > block.timestamp - ) { + // If there are outstanding long withdrawal shares, we attribute a + // proportional amount of the proceeds to the withdrawal pool and the + // active LPs. Otherwise, we use simplified accounting that has the same + // behavior but is more gas efficient. Since the difference between the + // base reserves and the longs outstanding stays the same or gets + // larger, we don't need to verify the reserves invariants. + if (longWithdrawalSharesOutstanding > 0) { // Calculate the effect that the trade has on the pool's APR. uint256 apr = HyperdriveMath.calculateAPRFromReserves( shareReserves.sub(_shareProceeds), @@ -749,85 +740,11 @@ contract Hyperdrive is MultiToken { timeStretch ); } else { - if (_maturityTime <= block.timestamp) { - longsMatured -= _bondAmount; - } shareReserves -= _shareProceeds; bondReserves += _poolBondDelta; } } - /// @dev Pays out the profits from matured longs to the withdrawal pool. - /// @param _bondAmount The amount of longs that have matured. - /// @param _sharePrice The current share price. - /// @param _maturityTime The maturity time of the longs. - function _applyMaturedLongsPayout( - uint256 _bondAmount, - uint256 _sharePrice, - uint256 _maturityTime - ) internal { - // Increase the amount of longs that have matured. - longsMatured += _bondAmount; - - if (longWithdrawalSharesOutstanding > 0) { - // Calculate the current pool APR. - uint256 apr = HyperdriveMath.calculateAPRFromReserves( - shareReserves, - bondReserves, - totalSupply[AssetId._LP_ASSET_ID], - initialSharePrice, - positionDuration, - timeStretch - ); - - // TODO: We could have a helper function for this. - // - // Since longs are backdated to the beginning of the checkpoint and - // interest only begins accruing when the longs are opened, we - // exclude the first checkpoint from LP withdrawal payouts. For most - // pools the difference will not be meaningful, and in edge cases, - // fees can be tuned to offset the problem. - uint256 openSharePrice = checkpoints[ - (_maturityTime - positionDuration) + checkpointDuration - ]; - - // Apply the LP proceeds from the trade proportionally to the long - // withdrawal shares. The accounting for these proceeds is identical - // to the close short accounting because LPs take the short position - // when longs are opened; however, we can make some simplifications - // since the longs have matured. The math for the withdrawal proceeds is - // given by: - // - // proceeds = c * (dy / c_0 - dy / c) * (min(b_x, dy) / dy) - // = (c / c_0 - 1) * dy * (min(b_x, dy) / dy) - // = (c / c_0 - 1) * min(b_x, dy) - uint256 withdrawalAmount = longWithdrawalSharesOutstanding < - _bondAmount - ? longWithdrawalSharesOutstanding - : _bondAmount; - uint256 withdrawalProceeds = ( - _sharePrice.divDown(openSharePrice).sub(FixedPointMath.ONE_18) - ).mulDown(withdrawalAmount); - longWithdrawalSharesOutstanding -= withdrawalAmount; - longWithdrawalShareProceeds += withdrawalProceeds; - - // Apply the withdrawal payouts to the reserves. These updates reflect - // the fact that some of the reserves will be attributed to the - // withdrawal pool. The math for the share reserves update is given by: - // - // z -= (c / c_0 - 1) * min(b_x, dy) / c - shareReserves -= withdrawalProceeds.divDown(_sharePrice); - bondReserves = HyperdriveMath.calculateBondReserves( - shareReserves, - totalSupply[AssetId._LP_ASSET_ID], - initialSharePrice, - apr, - positionDuration, - timeStretch - ); - } - } - /// @dev Applies the trading deltas from a closed short to the reserves and /// the withdrawal pool. /// @param _bondAmount The amount of shorts that were closed. @@ -836,28 +753,22 @@ contract Hyperdrive is MultiToken { /// 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 bonds. function _applyCloseShort( uint256 _bondAmount, uint256 _poolBondDelta, uint256 _sharePayment, - uint256 _sharePrice, - uint256 _maturityTime + uint256 _sharePrice ) internal { // Decrease the amount of shorts outstanding. shortsOutstanding -= _bondAmount; - // If there are outstanding short withdrawal shares and the bonds haven't - // matured yet, we attribute a proportional amount of the proceeds to - // the withdrawal pool and the active LPs. Otherwise, we use simplified - // accounting that has the same behavior but is more gas efficient. - // Since the difference between the base reserves and the longs - // outstanding stays the same or gets larger, we don't need to verify - // the reserves invariants. - if ( - shortWithdrawalSharesOutstanding > 0 && - _maturityTime > block.timestamp - ) { + // If there are outstanding short withdrawal shares, we attribute a + // proportional amount of the proceeds to the withdrawal pool and the + // active LPs. Otherwise, we use simplified accounting that has the same + // behavior but is more gas efficient. Since the difference between the + // base reserves and the longs outstanding stays the same or gets + // larger, we don't need to verify the reserves invariants. + if (shortWithdrawalSharesOutstanding > 0) { // Calculate the effect that the trade has on the pool's APR. uint256 apr = HyperdriveMath.calculateAPRFromReserves( shareReserves.add(_sharePayment), @@ -902,65 +813,11 @@ contract Hyperdrive is MultiToken { timeStretch ); } else { - if (_maturityTime <= block.timestamp) { - shortsMatured -= _bondAmount; - } shareReserves += _sharePayment; bondReserves -= _poolBondDelta; } } - /// @dev Pays out the profits from matured shorts to the withdrawal pool. - /// @param _bondAmount The amount of longs that have matured. - function _applyMaturedShortsPayout(uint256 _bondAmount) internal { - // Increase the amount of shorts that have matured. - shortsMatured += _bondAmount; - - if (shortWithdrawalSharesOutstanding > 0) { - // Calculate the effect that the trade has on the pool's APR. - uint256 apr = HyperdriveMath.calculateAPRFromReserves( - shareReserves, - bondReserves, - totalSupply[AssetId._LP_ASSET_ID], - initialSharePrice, - positionDuration, - timeStretch - ); - - // Apply the LP proceeds from the trade proportionally to the short - // withdrawal pool. The accounting for these proceeds is identical - // to the close short accounting because LPs take on a long position when - // shorts are opened; however, we can make some simplifications - // since the longs have matured. The math for the withdrawal proceeds is - // given by: - // - // proceeds = c * (dy / c) * (min(b_y, dy) / dy) - // = dy * (min(b_y, dy) / dy) - // = min(b_y, dy) - uint256 withdrawalAmount = shortWithdrawalSharesOutstanding < - _bondAmount - ? shortWithdrawalSharesOutstanding - : _bondAmount; - shortWithdrawalSharesOutstanding -= withdrawalAmount; - shortWithdrawalShareProceeds += withdrawalAmount; - - // Apply the trading deltas to the reserves. These updates reflect - // the fact that some of the reserves will be attributed to the - // withdrawal pool. The math for the share reserves update is given by: - // - // z -= min(b_y, dy) - shareReserves -= withdrawalAmount; - bondReserves = HyperdriveMath.calculateBondReserves( - shareReserves, - totalSupply[AssetId._LP_ASSET_ID], - initialSharePrice, - apr, - positionDuration, - timeStretch - ); - } - } - // TODO: If we find that this checkpointing flow is too heavy (which is // quite possible), we can store the share price and update some key metrics // about matured positions and add a poking system that performs the rest of @@ -987,7 +844,12 @@ contract Hyperdrive is MultiToken { AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, _checkpointTime) ]; if (maturedLongsAmount > 0) { - _applyMaturedLongsPayout( + // TODO: YieldSpaceMath currently returns a positive quantity at + // redemption. With this in mind, this will represent a + // slight inaccuracy until this problem is fixed. + _applyCloseLong( + maturedLongsAmount, + 0, maturedLongsAmount, _sharePrice, _checkpointTime @@ -999,7 +861,15 @@ contract Hyperdrive is MultiToken { AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, _checkpointTime) ]; if (maturedShortsAmount > 0) { - _applyMaturedShortsPayout(maturedShortsAmount); + // TODO: YieldSpaceMath currently returns a positive quantity at + // redemption. With this in mind, this will represent a + // slight inaccuracy until this problem is fixed. + _applyCloseShort( + maturedShortsAmount, + 0, + maturedShortsAmount, + _sharePrice + ); } return checkpoints[_checkpointTime]; diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index f4cc57402..c329e1a25 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -280,9 +280,7 @@ library HyperdriveMath { /// @param _shareReserves The pool's share reserves. /// @param _lpTotalSupply The pool's total supply of LP shares. /// @param _longsOutstanding The amount of longs that haven't been closed. - /// @param _longsMatured The amount of outstanding longs that have matured. /// @param _shortsOutstanding The amount of shorts that haven't been closed. - /// @param _shortsMatured The amount of outstanding shorts that have matured. /// @param _sharePrice The pool's share price. /// @return shares The amount of base shares released. /// @return longWithdrawalShares The amount of long withdrawal shares @@ -294,9 +292,7 @@ library HyperdriveMath { uint256 _shareReserves, uint256 _lpTotalSupply, uint256 _longsOutstanding, - uint256 _longsMatured, uint256 _shortsOutstanding, - uint256 _shortsMatured, uint256 _sharePrice ) internal @@ -313,13 +309,10 @@ library HyperdriveMath { shares = _shareReserves .sub(_longsOutstanding.divDown(_sharePrice)) .mulDown(poolFactor); - // (longsOutstanding - longsMatured) * (dl / l) - longWithdrawalShares = (_longsOutstanding.sub(_longsMatured)).mulDown( - poolFactor - ); - // (shortsOutstanding - shortsMatured) * (dl / l) - shortWithdrawalShares = (_shortsOutstanding.sub(_shortsMatured)) - .mulDown(poolFactor); + // longsOutstanding * (dl / l) + longWithdrawalShares = _longsOutstanding.mulDown(poolFactor); + // shortsOutstanding * (dl / l) + shortWithdrawalShares = _shortsOutstanding.mulDown(poolFactor); return (shares, longWithdrawalShares, shortWithdrawalShares); } } From 089318dee4ea530f53fa926c885d35097ede705a Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sat, 11 Feb 2023 17:10:52 -0600 Subject: [PATCH 15/31] Wrote a test for initialize --- contracts/Hyperdrive.sol | 11 +++-- package.json | 2 +- test/Hyperdrive.t.sol | 75 ++++++++++++++++++++----------- test/mocks/ERC20Mintable.sol | 17 +++++++ test/mocks/MockHyperdrive.sol | 85 +++++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+), 32 deletions(-) create mode 100644 test/mocks/ERC20Mintable.sol create mode 100644 test/mocks/MockHyperdrive.sol diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 22ee24fef..4c9ecb18d 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -15,7 +15,7 @@ import { MultiToken } from "contracts/MultiToken.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -contract Hyperdrive is MultiToken { +abstract contract Hyperdrive is MultiToken { using FixedPointMath for uint256; /// Tokens /// @@ -109,7 +109,6 @@ contract Hyperdrive is MultiToken { } /// Yield Source /// - // In order to deploy a yield source implement must be written which implements the following methods /// @notice Transfers base from the user and commits it to the yield source. /// @param amount The amount of base to deposit. @@ -117,7 +116,7 @@ contract Hyperdrive is MultiToken { /// @return sharePrice The share price at time of deposit. function deposit( uint256 amount - ) internal virtual returns (uint256 sharesMinted, uint256 sharePrice) {} + ) internal virtual returns (uint256 sharesMinted, uint256 sharePrice); /// @notice Withdraws shares from the yield source and sends the base /// released to the destination. @@ -128,11 +127,11 @@ contract Hyperdrive is MultiToken { function withdraw( uint256 shares, address destination - ) internal virtual returns (uint256 amountWithdrawn, uint256 sharePrice) {} + ) internal virtual returns (uint256 amountWithdrawn, uint256 sharePrice); ///@notice Loads the share price from the yield source ///@return sharePrice The current share price. - function pricePerShare() internal virtual returns (uint256 sharePrice) {} + function pricePerShare() internal virtual returns (uint256 sharePrice); /// LP /// @@ -159,7 +158,7 @@ contract Hyperdrive is MultiToken { bondReserves = HyperdriveMath.calculateBondReserves( shares, shares, - sharePrice, + initialSharePrice, _apr, positionDuration, timeStretch diff --git a/package.json b/package.json index 02866743f..2abf10cf5 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "clean": "forge clean", "lint": "yarn solhint && yarn style-check && yarn spell-check && echo \"done\"", "prettier": "npx prettier --write .", - "test": "forge test", + "test": "forge test -vvv", "solhint": "npx solhint -f table contracts/*.sol contracts/**/*.sol", "spell-check": "npx cspell ./**/**/**.sol --gitignore", "style-check": "npx prettier --check .", diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index b7cb4cdbf..b5eacc2fb 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -1,40 +1,65 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.13; -import { ERC20PresetFixedSupply } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import { Test } from "forge-std/Test.sol"; import { ForwarderFactory } from "contracts/ForwarderFactory.sol"; -import { Hyperdrive } from "contracts/Hyperdrive.sol"; -import "contracts/libraries/FixedPointMath.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"; contract HyperdriveTest is Test { address alice = address(uint160(uint256(keccak256("alice")))); - ERC20PresetFixedSupply baseToken; - Hyperdrive hyperdrive; + ERC20Mintable baseToken; + MockHyperdrive hyperdrive; function setUp() public { - vm.prank(alice); - - // Instantiate the tokens. - bytes32 linkerCodeHash = bytes32(0); - ForwarderFactory forwarderFactory = new ForwarderFactory(); - baseToken = new ERC20PresetFixedSupply( - "DAI Stablecoin", - "DAI", - 10.0e18, - alice - ); + vm.startPrank(alice); + + // Instantiate the base token. + baseToken = new ERC20Mintable(); // Instantiate Hyperdrive. - hyperdrive = new Hyperdrive({ - _linkerCodeHash: linkerCodeHash, - _linkerFactoryAddress: address(forwarderFactory), - _baseToken: baseToken, - _initialSharePrice: FixedPointMath.ONE_18, - _positionDuration: 365 days, - _checkpointDuration: 1 days, - _timeStretch: 22.186877016851916266e18 - }); + hyperdrive = new MockHyperdrive( + baseToken, + FixedPointMath.ONE_18, + 365 days, + 1 days, + 22.186877016851916266e18 + ); + } + + /// Initialize /// + + // FIXME: It would be good to fuzz this. We should also try this with + // different values for initial share price and share price. + function test_initialize() external { + // Mint some base for Alice and approve the Hyperdrive contract. + uint256 contribution = 1000.0e18; + baseToken.mint(contribution); + baseToken.approve(address(hyperdrive), contribution); + + // Initialize Hyperdrive. + uint256 apr = 0.5e18; + hyperdrive.initialize(contribution, apr); + + // Ensure that the pool's APR is approximately equal to the target APR + // within 3 decimals of precision. + uint256 poolApr = HyperdriveMath.calculateAPRFromReserves( + hyperdrive.shareReserves(), + hyperdrive.bondReserves(), + hyperdrive.totalSupply(AssetId._LP_ASSET_ID), + hyperdrive.initialSharePrice(), + hyperdrive.positionDuration(), + hyperdrive.timeStretch() + ); + assertApproxEqAbs(poolApr, apr, 1.0e3); + + // FIXME: Ensure that Alice received the right amount of LP shares. + + // FIXME: Have Bob try to initialize and ensure that it fails. } } diff --git a/test/mocks/ERC20Mintable.sol b/test/mocks/ERC20Mintable.sol new file mode 100644 index 000000000..636287957 --- /dev/null +++ b/test/mocks/ERC20Mintable.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.15; + +import { ERC20Burnable } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ERC20Mintable is ERC20 { + constructor() ERC20("Base", "BASE") {} + + function mint(uint256 amount) external { + _mint(msg.sender, amount); + } + + function burn(uint256 amount) external { + _burn(msg.sender, amount); + } +} diff --git a/test/mocks/MockHyperdrive.sol b/test/mocks/MockHyperdrive.sol new file mode 100644 index 000000000..3f2c9d0a9 --- /dev/null +++ b/test/mocks/MockHyperdrive.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.15; + +import { ERC20PresetMinterPauser } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import { ForwarderFactory } from "contracts/ForwarderFactory.sol"; +import { Hyperdrive } from "contracts/Hyperdrive.sol"; +import { FixedPointMath } from "contracts/libraries/FixedPointMath.sol"; +import { Errors } from "contracts/libraries/Errors.sol"; +import { ERC20Mintable } from "test/mocks/ERC20Mintable.sol"; + +contract MockHyperdrive is Hyperdrive { + using FixedPointMath for uint256; + + uint256 internal _sharePrice; + + constructor( + ERC20Mintable baseToken, + uint256 _initialSharePrice, + uint256 _positionDuration, + uint256 _checkpointDuration, + uint256 _timeStretch + ) + Hyperdrive( + bytes32(0), + address(new ForwarderFactory()), + baseToken, + _initialSharePrice, + _positionDuration, + _checkpointDuration, + _timeStretch + ) + { + _sharePrice = _initialSharePrice; + } + + /// Mocks /// + + error InvalidSharePrice(); + + function setSharePrice(uint256 sharePrice) external { + if (sharePrice <= _sharePrice) { + revert InvalidSharePrice(); + } + + // Update the share price and accrue interest. + ERC20Mintable(address(baseToken)).mint( + (sharePrice.sub(_sharePrice)).mulDown( + baseToken.balanceOf(address(this)) + ) + ); + _sharePrice = sharePrice; + } + + /// Overrides /// + + function deposit( + uint256 amount + ) internal override returns (uint256, uint256) { + bool success = baseToken.transferFrom( + msg.sender, + address(this), + amount + ); + if (!success) { + revert Errors.TransferFailed(); + } + return (amount.divDown(_sharePrice), _sharePrice); + } + + function withdraw( + uint256 shares, + address destination + ) internal override returns (uint256, uint256) { + uint256 amountWithdrawn = shares.mulDown(_sharePrice); + bool success = baseToken.transfer(destination, amountWithdrawn); + if (!success) { + revert Errors.TransferFailed(); + } + return (amountWithdrawn, _sharePrice); + } + + function pricePerShare() internal view override returns (uint256) { + return _sharePrice; + } +} From 12466013399dc4207f526702a160afe5cf83a296 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sat, 11 Feb 2023 17:59:27 -0600 Subject: [PATCH 16/31] Improved the initialization tests --- test/Hyperdrive.t.sol | 39 +++++++++++++++++++++++++++++------ test/mocks/MockHyperdrive.sol | 4 ++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index b5eacc2fb..80ddc3adc 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -5,13 +5,17 @@ import { Test } from "forge-std/Test.sol"; import { ForwarderFactory } from "contracts/ForwarderFactory.sol"; import { ForwarderFactory } from "contracts/ForwarderFactory.sol"; 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 { ERC20Mintable } from "test/mocks/ERC20Mintable.sol"; import { MockHyperdrive } from "test/mocks/MockHyperdrive.sol"; contract HyperdriveTest is Test { + using FixedPointMath for uint256; + address alice = address(uint160(uint256(keccak256("alice")))); + address bob = address(uint160(uint256(keccak256("bob")))); ERC20Mintable baseToken; MockHyperdrive hyperdrive; @@ -36,13 +40,13 @@ contract HyperdriveTest is Test { // FIXME: It would be good to fuzz this. We should also try this with // different values for initial share price and share price. - function test_initialize() external { - // Mint some base for Alice and approve the Hyperdrive contract. + function test_initialization_success() external { + // Initialize Hyperdrive. + vm.stopPrank(); + vm.startPrank(alice); uint256 contribution = 1000.0e18; baseToken.mint(contribution); baseToken.approve(address(hyperdrive), contribution); - - // Initialize Hyperdrive. uint256 apr = 0.5e18; hyperdrive.initialize(contribution, apr); @@ -58,8 +62,31 @@ contract HyperdriveTest is Test { ); assertApproxEqAbs(poolApr, apr, 1.0e3); - // FIXME: Ensure that Alice received the right amount of LP shares. + // Ensure that Alice's base balance has been depleted and that Alice + // received some LP tokens. + assertEq(baseToken.balanceOf(alice), 0); + assertEq( + hyperdrive.totalSupply(AssetId._LP_ASSET_ID), + contribution.divDown(hyperdrive.getSharePrice()) + ); + } + + function test_initialization_failure() external { + // Initialize the pool with Alice. + vm.stopPrank(); + vm.startPrank(alice); + uint256 apr = 0.5e18; + uint256 contribution = 1000.0e18; + baseToken.mint(contribution); + baseToken.approve(address(hyperdrive), contribution); + hyperdrive.initialize(contribution, apr); - // FIXME: Have Bob try to initialize and ensure that it fails. + // Attempt to initialize the pool a second time. This should fail. + vm.stopPrank(); + vm.startPrank(bob); + baseToken.mint(contribution); + baseToken.approve(address(hyperdrive), contribution); + vm.expectRevert(Errors.PoolAlreadyInitialized.selector); + hyperdrive.initialize(contribution, apr); } } diff --git a/test/mocks/MockHyperdrive.sol b/test/mocks/MockHyperdrive.sol index 3f2c9d0a9..2b4481e2a 100644 --- a/test/mocks/MockHyperdrive.sol +++ b/test/mocks/MockHyperdrive.sol @@ -37,6 +37,10 @@ contract MockHyperdrive is Hyperdrive { error InvalidSharePrice(); + function getSharePrice() external view returns (uint256) { + return _sharePrice; + } + function setSharePrice(uint256 sharePrice) external { if (sharePrice <= _sharePrice) { revert InvalidSharePrice(); From c5cc90b9c38dfe04cbe11bfb9e267ce8f131b91e Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sat, 11 Feb 2023 20:34:51 -0600 Subject: [PATCH 17/31] Added tests for opening longs --- contracts/Hyperdrive.sol | 42 +++++- contracts/libraries/HyperdriveMath.sol | 8 +- contracts/libraries/YieldSpaceMath.sol | 2 +- test/Hyperdrive.t.sol | 197 ++++++++++++++++++++++--- 4 files changed, 220 insertions(+), 29 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 4c9ecb18d..0206608d4 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -131,7 +131,7 @@ abstract contract Hyperdrive is MultiToken { ///@notice Loads the share price from the yield source ///@return sharePrice The current share price. - function pricePerShare() internal virtual returns (uint256 sharePrice); + function pricePerShare() internal view virtual returns (uint256 sharePrice); /// LP /// @@ -652,6 +652,46 @@ abstract contract Hyperdrive is MultiToken { } } + /// Getters /// + + /// @notice Gets info about the pool's reserves and other state that is + /// important to evaluate potential trades. + /// @return shareReserves_ The share reserves. + /// @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 longsMatured_ The longs that haven't been closed but have + /// matured. + /// @return shortsOutstanding_ The shorts that haven't been closed. + /// @return shortsMatured_ The shorts that haven't been closed. but have + /// matured. + function getPoolInfo() + external + view + returns ( + uint256 shareReserves_, + uint256 bondReserves_, + uint256 lpTotalSupply, + uint256 sharePrice, + uint256 longsOutstanding_, + uint256 longsMatured_, + uint256 shortsOutstanding_, + uint256 shortsMatured_ + ) + { + return ( + shareReserves, + bondReserves, + totalSupply[AssetId._LP_ASSET_ID], + pricePerShare(), + longsOutstanding, + longsMatured, + shortsOutstanding, + shortsMatured + ); + } + /// Helpers /// /// @dev Applies the trading deltas from a closed long to the reserves and diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index c329e1a25..75b9e53f5 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -34,9 +34,9 @@ library HyperdriveMath { uint256 _positionDuration, uint256 _timeStretch ) internal pure returns (uint256 apr) { - // NOTE: This calculation is automatically scaled in the divDown operation + // NOTE: Using divDown to convert to fixed point format. uint256 t = _positionDuration.divDown(365 days); - uint256 tau = t.divDown(_timeStretch); + uint256 tau = t.mulDown(_timeStretch); // ((y + s) / (mu * z)) ** -tau uint256 spotPrice = _initialSharePrice .mulDown(_shareReserves) @@ -68,9 +68,9 @@ library HyperdriveMath { uint256 _positionDuration, uint256 _timeStretch ) internal pure returns (uint256 bondReserves) { - // NOTE: This calculation is automatically scaled in the divDown operation + // NOTE: Using divDown to convert to fixed point format. uint256 t = _positionDuration.divDown(365 days); - uint256 tau = t.divDown(_timeStretch); + uint256 tau = t.mulDown(_timeStretch); // (1 + apr * t) ** (1 / tau) uint256 interestFactor = FixedPointMath.ONE_18.add(_apr.mulDown(t)).pow( FixedPointMath.ONE_18.divDown(tau) diff --git a/contracts/libraries/YieldSpaceMath.sol b/contracts/libraries/YieldSpaceMath.sol index 27053f428..cb27c48a0 100644 --- a/contracts/libraries/YieldSpaceMath.sol +++ b/contracts/libraries/YieldSpaceMath.sol @@ -1,7 +1,7 @@ /// SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.15; -import "contracts/libraries/FixedPointMath.sol"; +import { FixedPointMath } from "contracts/libraries/FixedPointMath.sol"; // FIXME: This doesn't compute the fee but maybe it should. // diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index 80ddc3adc..49f757a49 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.13; +import { stdError } from "forge-std/StdError.sol"; import { Test } from "forge-std/Test.sol"; import { ForwarderFactory } from "contracts/ForwarderFactory.sol"; import { ForwarderFactory } from "contracts/ForwarderFactory.sol"; @@ -27,31 +28,70 @@ contract HyperdriveTest is Test { baseToken = new ERC20Mintable(); // Instantiate Hyperdrive. + uint256 timeStretch = FixedPointMath.ONE_18.divDown( + 22.186877016851916266e18 + ); hyperdrive = new MockHyperdrive( baseToken, FixedPointMath.ONE_18, 365 days, 1 days, - 22.186877016851916266e18 + timeStretch ); + + // Advance time so that Hyperdrive can look back more than a position + // duration. + vm.warp(365 days * 3); } - /// Initialize /// + /// initialize /// - // FIXME: It would be good to fuzz this. We should also try this with - // different values for initial share price and share price. - function test_initialization_success() external { - // Initialize Hyperdrive. + function initialize( + address lp, + uint256 apr, + uint256 contribution + ) internal { vm.stopPrank(); - vm.startPrank(alice); - uint256 contribution = 1000.0e18; + vm.startPrank(lp); + + // Initialize the pool. baseToken.mint(contribution); baseToken.approve(address(hyperdrive), contribution); + hyperdrive.initialize(contribution, apr); + } + + function test_initialize_failure() external { uint256 apr = 0.5e18; + uint256 contribution = 1000.0e18; + + // Initialize the pool with Alice. + initialize(alice, apr, contribution); + + // Attempt to initialize the pool a second time. This should fail. + vm.stopPrank(); + vm.startPrank(bob); + baseToken.mint(contribution); + baseToken.approve(address(hyperdrive), contribution); + vm.expectRevert(Errors.PoolAlreadyInitialized.selector); hyperdrive.initialize(contribution, apr); + } + + // TODO: We need a test that verifies that the quoted APR is the same as the + // realized APR of making a small trade on the pool. This should be part of + // the open long testing. + // + // TODO: This should ultimately be a fuzz test that fuzzes over the initial + // share price, the APR, the contribution, the position duration, and other + // parameters that can have an impact on the pool's APR. + function test_initialize_success() external { + uint256 apr = 0.05e18; + uint256 contribution = 1000e18; + + // Initialize the pool with Alice. + initialize(alice, apr, contribution); // Ensure that the pool's APR is approximately equal to the target APR - // within 3 decimals of precision. + // within 15 decimals of precision. uint256 poolApr = HyperdriveMath.calculateAPRFromReserves( hyperdrive.shareReserves(), hyperdrive.bondReserves(), @@ -60,33 +100,144 @@ contract HyperdriveTest is Test { hyperdrive.positionDuration(), hyperdrive.timeStretch() ); - assertApproxEqAbs(poolApr, apr, 1.0e3); + assertApproxEqAbs(poolApr, apr, 1e3); // Ensure that Alice's base balance has been depleted and that Alice // received some LP tokens. assertEq(baseToken.balanceOf(alice), 0); + assertEq(baseToken.balanceOf(address(hyperdrive)), contribution); assertEq( hyperdrive.totalSupply(AssetId._LP_ASSET_ID), contribution.divDown(hyperdrive.getSharePrice()) ); } - function test_initialization_failure() external { - // Initialize the pool with Alice. + /// openLong /// + + function calculateAPRFromRealizedPrice( + uint256 baseAmount, + uint256 bondAmount, + uint256 timeRemaining, + uint256 positionDuration + ) internal pure returns (uint256) { + // apr = (dy - dx) / (dx * t) + uint256 t = timeRemaining.divDown(positionDuration); + return (bondAmount.sub(baseAmount)).divDown(baseAmount.mulDown(t)); + } + + function test_open_long_zero_amount() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Attempt to purchase bonds with zero base. This should fail. vm.stopPrank(); - vm.startPrank(alice); - uint256 apr = 0.5e18; - uint256 contribution = 1000.0e18; - baseToken.mint(contribution); - baseToken.approve(address(hyperdrive), contribution); - hyperdrive.initialize(contribution, apr); + vm.startPrank(bob); + vm.expectRevert(Errors.ZeroAmount.selector); + hyperdrive.openLong(0); + } - // Attempt to initialize the pool a second time. This should fail. + function test_open_long_extreme_amount() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // TODO: The bond reserves are too large. We should special case + // initialization and see if that fixes the problem. + // + // Attempt to purchase more bonds than exist in the reserves. This + // should fail. vm.stopPrank(); vm.startPrank(bob); - baseToken.mint(contribution); - baseToken.approve(address(hyperdrive), contribution); - vm.expectRevert(Errors.PoolAlreadyInitialized.selector); - hyperdrive.initialize(contribution, apr); + uint256 baseAmount = hyperdrive.bondReserves(); + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + vm.expectRevert(stdError.arithmeticError); + hyperdrive.openLong(baseAmount); + } + + function test_open_long() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Get the values of the reserves before the purchase. + ( + uint256 shareReservesBefore, + uint256 bondReservesBefore, + uint256 lpTotalSupplyBefore, + uint256 sharePriceBefore, + uint256 longsOutstandingBefore, + uint256 longsMaturedBefore, + uint256 shortsOutstandingBefore, + uint256 shortsMaturedBefore + ) = hyperdrive.getPoolInfo(); + + // TODO: Small base amounts result in higher than quoted APRs. We should + // first investigate the math to see if there are obvious simplifications + // to be made, and then consider increasing the amount of precision in + // used in our fixed rate format. + // + // Purchase a small amount of bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 baseAmount = 100e18; + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + hyperdrive.openLong(baseAmount); + + // Verify the base transfers. + assertEq(baseToken.balanceOf(bob), 0); + assertEq( + baseToken.balanceOf(address(hyperdrive)), + contribution + baseAmount + ); + + // Verify that Bob received an acceptable amount of bonds. Since the + // base amount is very low relative to the pool's liquidity, the implied + // APR should be approximately equal to the pool's APR. + uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + + 365 days; + uint256 bondAmount = hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + bob + ); + uint256 realizedApr = calculateAPRFromRealizedPrice( + baseAmount, + bondAmount, + maturityTime - block.timestamp, + 365 days + ); + // TODO: This tolerance seems too high. + assertApproxEqAbs(realizedApr, apr, 1e10); + + // Verify that the reserves were updated correctly. + ( + uint256 shareReservesAfter, + uint256 bondReservesAfter, + uint256 lpTotalSupplyAfter, + uint256 sharePriceAfter, + uint256 longsOutstandingAfter, + uint256 longsMaturedAfter, + uint256 shortsOutstandingAfter, + uint256 shortsMaturedAfter + ) = hyperdrive.getPoolInfo(); + assertEq( + shareReservesAfter, + shareReservesBefore + baseAmount.divDown(sharePriceBefore) + ); + assertEq(bondReservesAfter, bondReservesBefore - bondAmount); + assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); + assertEq(sharePriceAfter, sharePriceBefore); + assertEq(longsOutstandingAfter, longsOutstandingBefore + bondAmount); + assertEq(longsMaturedAfter, longsMaturedBefore); + assertEq(shortsOutstandingAfter, shortsOutstandingBefore); + assertEq(shortsMaturedAfter, shortsMaturedBefore); } } From 6ae5e3a98c7ca6cab4a3b86906857af4b327ae85 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sun, 12 Feb 2023 09:15:43 -0600 Subject: [PATCH 18/31] Updated the pool's initialization logic --- contracts/Hyperdrive.sol | 10 ++++--- contracts/libraries/HyperdriveMath.sol | 36 +++++++++++++++++++++++--- test/Hyperdrive.t.sol | 15 ++++------- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 0206608d4..5f03a04e1 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -155,9 +155,9 @@ abstract contract Hyperdrive is MultiToken { // Update the reserves. The bond reserves are calculated so that the // pool is initialized with the target APR. shareReserves = shares; - bondReserves = HyperdriveMath.calculateBondReserves( - shares, + bondReserves = HyperdriveMath.calculateInitialBondReserves( shares, + sharePrice, initialSharePrice, _apr, positionDuration, @@ -167,7 +167,11 @@ abstract contract Hyperdrive is MultiToken { // Mint LP shares to the initializer. // TODO - Should we index the lp share and virtual reserve to shares or to underlying? // I think in the case where price per share < 1 there may be a problem. - _mint(AssetId._LP_ASSET_ID, msg.sender, shares); + _mint( + AssetId._LP_ASSET_ID, + msg.sender, + sharePrice.mulDown(shares).add(bondReserves) + ); } // TODO: Add slippage protection. diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index 75b9e53f5..047062318 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -47,9 +47,39 @@ library HyperdriveMath { FixedPointMath.ONE_18.sub(spotPrice).divDown(spotPrice.mulDown(t)); } - // TODO: There is likely a more efficient formulation for when the rate is - // based on the existing share and bond reserves. - // + /// @dev Calculates the initial bond reserves assuming that the initial LP + /// receives LP shares amounting to c * z + y. + /// @param _shareReserves The pool's share reserves. + /// @param _sharePrice The pool's share price. + /// @param _initialSharePrice The pool's initial share price. + /// @param _apr The pool's APR. + /// @param _positionDuration The amount of time until maturity in seconds. + /// @param _timeStretch The time stretch parameter. + /// @return bondReserves The bond reserves that make the pool have a + /// specified APR. + function calculateInitialBondReserves( + uint256 _shareReserves, + uint256 _sharePrice, + uint256 _initialSharePrice, + uint256 _apr, + uint256 _positionDuration, + uint256 _timeStretch + ) internal pure returns (uint256 bondReserves) { + // NOTE: Using divDown to convert to fixed point format. + uint256 t = _positionDuration.divDown(365 days); + uint256 tau = t.mulDown(_timeStretch); + // mu * (1 + apr * t) ** (1 / tau) - c + uint256 rhs = _initialSharePrice + .mulDown( + FixedPointMath.ONE_18.add(_apr.mulDown(t)).pow( + FixedPointMath.ONE_18.divDown(tau) + ) + ) + .sub(_sharePrice); + // (z / 2) * (mu * (1 + apr * t) ** (1 / tau) - c) + return _shareReserves.divDown(2 * FixedPointMath.ONE_18).mulDown(rhs); + } + /// @dev Calculates the bond reserves that will make the pool have a /// specified APR. /// @param _shareReserves The pool's share reserves. diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index 49f757a49..13373e879 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -90,8 +90,7 @@ contract HyperdriveTest is Test { // Initialize the pool with Alice. initialize(alice, apr, contribution); - // Ensure that the pool's APR is approximately equal to the target APR - // within 15 decimals of precision. + // Ensure that the pool's APR is approximately equal to the target APR. uint256 poolApr = HyperdriveMath.calculateAPRFromReserves( hyperdrive.shareReserves(), hyperdrive.bondReserves(), @@ -100,7 +99,7 @@ contract HyperdriveTest is Test { hyperdrive.positionDuration(), hyperdrive.timeStretch() ); - assertApproxEqAbs(poolApr, apr, 1e3); + assertApproxEqAbs(poolApr, apr, 1e1); // 17 decimals of precision // Ensure that Alice's base balance has been depleted and that Alice // received some LP tokens. @@ -108,7 +107,7 @@ contract HyperdriveTest is Test { assertEq(baseToken.balanceOf(address(hyperdrive)), contribution); assertEq( hyperdrive.totalSupply(AssetId._LP_ASSET_ID), - contribution.divDown(hyperdrive.getSharePrice()) + contribution + hyperdrive.bondReserves() ); } @@ -146,11 +145,7 @@ contract HyperdriveTest is Test { uint256 contribution = 500_000_000e18; initialize(alice, apr, contribution); - // TODO: The bond reserves are too large. We should special case - // initialization and see if that fixes the problem. - // - // Attempt to purchase more bonds than exist in the reserves. This - // should fail. + // Attempt to purchase more bonds than exist. This should fail. vm.stopPrank(); vm.startPrank(bob); uint256 baseAmount = hyperdrive.bondReserves(); @@ -187,7 +182,7 @@ contract HyperdriveTest is Test { // Purchase a small amount of bonds. vm.stopPrank(); vm.startPrank(bob); - uint256 baseAmount = 100e18; + uint256 baseAmount = 10e18; baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); hyperdrive.openLong(baseAmount); From 223957a307cd2d40b3f7911e6a44aa2d0f0fc9dc Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sun, 12 Feb 2023 09:33:29 -0600 Subject: [PATCH 19/31] Add some failure tests for "closeLong" --- contracts/Hyperdrive.sol | 4 +- contracts/libraries/AssetId.sol | 6 +++ contracts/libraries/Errors.sol | 2 +- test/Hyperdrive.t.sol | 76 +++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 5f03a04e1..bc256c0e3 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -413,7 +413,7 @@ abstract contract Hyperdrive is MultiToken { /// @notice Closes a long position with a specified maturity time. /// @param _maturityTime The maturity time of the short. /// @param _bondAmount The amount of longs to close. - function closeLong(uint32 _maturityTime, uint256 _bondAmount) external { + function closeLong(uint256 _maturityTime, uint256 _bondAmount) external { if (_bondAmount == 0) { revert Errors.ZeroAmount(); } @@ -544,7 +544,7 @@ abstract contract Hyperdrive is MultiToken { /// @notice Closes a short position with a specified maturity time. /// @param _maturityTime The maturity time of the short. /// @param _bondAmount The amount of shorts to close. - function closeShort(uint32 _maturityTime, uint256 _bondAmount) external { + function closeShort(uint256 _maturityTime, uint256 _bondAmount) external { if (_bondAmount == 0) { revert Errors.ZeroAmount(); } diff --git a/contracts/libraries/AssetId.sol b/contracts/libraries/AssetId.sol index 08fc96c50..14c37ef2a 100644 --- a/contracts/libraries/AssetId.sol +++ b/contracts/libraries/AssetId.sol @@ -36,6 +36,12 @@ library AssetId { uint256 _timestamp ) internal pure returns (uint256 id) { // [identifier: 8 bits][timestamp: 248 bits] + if ( + _timestamp > + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + ) { + revert Errors.InvalidTimestamp(); + } assembly { id := or(shl(0xf8, _prefix), _timestamp) } diff --git a/contracts/libraries/Errors.sol b/contracts/libraries/Errors.sol index b032e4129..9fb45aed6 100644 --- a/contracts/libraries/Errors.sol +++ b/contracts/libraries/Errors.sol @@ -41,5 +41,5 @@ library Errors { /// ############### /// ### AssetId ### /// ############### - error AssetIDCorruption(); + error InvalidTimestamp(); } diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index 13373e879..da3fce544 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -235,4 +235,80 @@ contract HyperdriveTest is Test { assertEq(shortsOutstandingAfter, shortsOutstandingBefore); assertEq(shortsMaturedAfter, shortsMaturedBefore); } + + /// Close Long /// + + function test_close_long_zero_amount() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Purchase some bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 baseAmount = 10e18; + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + hyperdrive.openLong(baseAmount); + + // Attempt to close zero longs. This should fail. + vm.stopPrank(); + vm.startPrank(bob); + vm.expectRevert(Errors.ZeroAmount.selector); + uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + + 365 days; + hyperdrive.closeLong(maturityTime, 0); + } + + function test_close_long_invalid_amount() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Purchase some bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 baseAmount = 10e18; + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + hyperdrive.openLong(baseAmount); + + // Attempt to close too many longs. This should fail. + vm.stopPrank(); + vm.startPrank(bob); + uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + + 365 days; + uint256 bondAmount = hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + bob + ); + vm.expectRevert(stdError.arithmeticError); + hyperdrive.closeLong(maturityTime, bondAmount + 1); + } + + function test_close_long_invalid_timestamp() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Purchase some bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 baseAmount = 10e18; + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + hyperdrive.openLong(baseAmount); + + // Attempt to use a timestamp greater than the maximum range. + vm.stopPrank(); + vm.startPrank(bob); + vm.expectRevert(Errors.InvalidTimestamp.selector); + hyperdrive.closeLong(uint256(type(uint248).max) + 1, 1); + } } From 99ccebe3d682ae00e942ce217c04fe168e97cba1 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sun, 12 Feb 2023 15:21:45 -0600 Subject: [PATCH 20/31] Wrote simple tests for "closeLong" and fixed bugs --- contracts/libraries/HyperdriveMath.sol | 4 - test/Hyperdrive.t.sol | 164 ++++++++++++++++++++++++- 2 files changed, 163 insertions(+), 5 deletions(-) diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index 047062318..f63c7493f 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -5,10 +5,6 @@ import { Errors } from "contracts/libraries/Errors.sol"; import { FixedPointMath } from "contracts/libraries/FixedPointMath.sol"; import { YieldSpaceMath } from "contracts/libraries/YieldSpaceMath.sol"; -// FIXME: The matrix of uses of flat+curve includes cases that should never -// occur. In particular, if isBondOut && t < 1 or isBondIn && t < 1, then the -// flat part refers to base tokens and the model doesn't make sense. -// /// @author Delve /// @title Hyperdrive /// @notice Math for the Hyperdrive pricing model. diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index da3fce544..05c52d5b6 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -162,7 +162,7 @@ contract HyperdriveTest is Test { uint256 contribution = 500_000_000e18; initialize(alice, apr, contribution); - // Get the values of the reserves before the purchase. + // Get the reserves before opening the long. ( uint256 shareReservesBefore, uint256 bondReservesBefore, @@ -311,4 +311,166 @@ contract HyperdriveTest is Test { vm.expectRevert(Errors.InvalidTimestamp.selector); hyperdrive.closeLong(uint256(type(uint248).max) + 1, 1); } + + function test_close_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); + + // Purchase some bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 baseAmount = 10e18; + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + hyperdrive.openLong(baseAmount); + + // Get the reserves before closing the long. + ( + uint256 shareReservesBefore, + uint256 bondReservesBefore, + uint256 lpTotalSupplyBefore, + uint256 sharePriceBefore, + uint256 longsOutstandingBefore, + uint256 longsMaturedBefore, + uint256 shortsOutstandingBefore, + uint256 shortsMaturedBefore + ) = hyperdrive.getPoolInfo(); + + // Immediately close the bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + + 365 days; + uint256 bondAmount = hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + bob + ); + hyperdrive.closeLong(maturityTime, bondAmount); + + // TODO: Bob receives more base than he started with. Fees should take + // care of this, but this should be investigating nonetheless. + // + // Verify that all of Bob's bonds were burned and that he has + // approximately as much base as he started with. + uint256 baseProceeds = baseToken.balanceOf(bob); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + bob + ), + 0 + ); + assertApproxEqAbs(baseProceeds, baseAmount, 1e10); + + // Verify that the reserves were updated correctly. Since this trade + // happens at the beginning of the term, the bond reserves should be + // increased by the full amount. + ( + uint256 shareReservesAfter, + uint256 bondReservesAfter, + uint256 lpTotalSupplyAfter, + uint256 sharePriceAfter, + uint256 longsOutstandingAfter, + uint256 longsMaturedAfter, + uint256 shortsOutstandingAfter, + uint256 shortsMaturedAfter + ) = hyperdrive.getPoolInfo(); + assertEq( + shareReservesAfter, + shareReservesBefore - baseProceeds.divDown(sharePriceBefore) + ); + assertEq(bondReservesAfter, bondReservesBefore + bondAmount); + assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); + assertEq(sharePriceAfter, sharePriceBefore); + assertEq(longsOutstandingAfter, longsOutstandingBefore - bondAmount); + assertEq(longsMaturedAfter, longsMaturedBefore); + assertEq(shortsOutstandingAfter, shortsOutstandingBefore); + assertEq(shortsMaturedAfter, shortsMaturedBefore); + } + + // TODO: Clean up these tests. + function test_close_long_redeem() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Purchase some bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 baseAmount = 10e18; + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + hyperdrive.openLong(baseAmount); + uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + + 365 days; + + // Get the reserves before closing the long. + ( + uint256 shareReservesBefore, + uint256 bondReservesBefore, + uint256 lpTotalSupplyBefore, + uint256 sharePriceBefore, + uint256 longsOutstandingBefore, + uint256 longsMaturedBefore, + uint256 shortsOutstandingBefore, + uint256 shortsMaturedBefore + ) = hyperdrive.getPoolInfo(); + + // The term passes. + vm.warp(block.timestamp + 365 days); + + // Redeem the bonds + vm.stopPrank(); + vm.startPrank(bob); + uint256 bondAmount = hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + bob + ); + hyperdrive.closeLong(maturityTime, bondAmount); + + // TODO: Bob receives more base than the bond amount. It appears that + // the yield space implementation returns a positive value even when + // the input is zero. + // + // Verify that all of Bob's bonds were burned and that he has + // approximately as much base as he started with. + uint256 baseProceeds = baseToken.balanceOf(bob); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + bob + ), + 0 + ); + assertApproxEqAbs(baseProceeds, bondAmount, 1e10); + + // Verify that the reserves were updated correctly. Since this trade + // is a redemption, there should be no changes to the bond reserves. + ( + uint256 shareReservesAfter, + uint256 bondReservesAfter, + uint256 lpTotalSupplyAfter, + uint256 sharePriceAfter, + uint256 longsOutstandingAfter, + uint256 longsMaturedAfter, + uint256 shortsOutstandingAfter, + uint256 shortsMaturedAfter + ) = hyperdrive.getPoolInfo(); + assertEq( + shareReservesAfter, + shareReservesBefore - baseProceeds.divDown(sharePriceBefore) + ); + assertEq(bondReservesAfter, bondReservesBefore); + assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); + assertEq(sharePriceAfter, sharePriceBefore); + assertEq(longsOutstandingAfter, longsOutstandingBefore - bondAmount); + assertEq(longsMaturedAfter, longsMaturedBefore); + assertEq(shortsOutstandingAfter, shortsOutstandingBefore); + assertEq(shortsMaturedAfter, shortsMaturedBefore); + } } From a17bd40b98d1d30fd94a95635e592dd8a486f3bd Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sun, 12 Feb 2023 18:39:51 -0600 Subject: [PATCH 21/31] Adds simple test cases for "openShort" --- test/Hyperdrive.t.sol | 117 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index 05c52d5b6..f5be862ee 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -473,4 +473,121 @@ contract HyperdriveTest is Test { assertEq(shortsOutstandingAfter, shortsOutstandingBefore); assertEq(shortsMaturedAfter, shortsMaturedBefore); } + + /// Open Short /// + + function test_open_short_zero_amount() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Attempt to short zero bonds. This should fail. + vm.stopPrank(); + vm.startPrank(bob); + vm.expectRevert(Errors.ZeroAmount.selector); + hyperdrive.openShort(0); + } + + function test_open_short_extreme_amount() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Attempt to short an extreme amount of bonds. This should fail. + vm.stopPrank(); + vm.startPrank(bob); + uint256 baseAmount = hyperdrive.shareReserves(); + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + vm.expectRevert(Errors.FixedPointMath_SubOverflow.selector); + hyperdrive.openShort(baseAmount * 2); + } + + function test_open_short() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Get the reserves before opening the short. + ( + uint256 shareReservesBefore, + uint256 bondReservesBefore, + uint256 lpTotalSupplyBefore, + uint256 sharePriceBefore, + uint256 longsOutstandingBefore, + uint256 longsMaturedBefore, + uint256 shortsOutstandingBefore, + uint256 shortsMaturedBefore + ) = hyperdrive.getPoolInfo(); + + // Short a small amount of bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 bondAmount = 10e18; + baseToken.mint(bondAmount); + baseToken.approve(address(hyperdrive), bondAmount); + hyperdrive.openShort(bondAmount); + + // Verify that Hyperdrive received the max loss and that Bob received + // the short tokens. + uint256 maxLoss = bondAmount - baseToken.balanceOf(bob); + uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + + 365 days; + assertEq( + baseToken.balanceOf(address(hyperdrive)), + contribution + maxLoss + ); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + maturityTime + ), + bob + ), + bondAmount + ); + + // Verify that Bob's short has an acceptable fixed rate. Since the bond + // amount is very low relative to the pool's liquidity, the implied APR + // should be approximately equal to the pool's APR. + uint256 baseAmount = bondAmount - maxLoss; + uint256 realizedApr = calculateAPRFromRealizedPrice( + baseAmount, + bondAmount, + maturityTime - block.timestamp, + 365 days + ); + // TODO: This tolerance seems too high. + assertApproxEqAbs(realizedApr, apr, 1e10); + + // Verify that the reserves were updated correctly. + ( + uint256 shareReservesAfter, + uint256 bondReservesAfter, + uint256 lpTotalSupplyAfter, + uint256 sharePriceAfter, + uint256 longsOutstandingAfter, + uint256 longsMaturedAfter, + uint256 shortsOutstandingAfter, + uint256 shortsMaturedAfter + ) = hyperdrive.getPoolInfo(); + assertEq( + shareReservesAfter, + shareReservesBefore - baseAmount.divDown(sharePriceBefore) + ); + assertEq(bondReservesAfter, bondReservesBefore + bondAmount); + assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); + assertEq(sharePriceAfter, sharePriceBefore); + assertEq(longsOutstandingAfter, longsOutstandingBefore); + assertEq(longsMaturedAfter, longsMaturedBefore); + assertEq(shortsOutstandingAfter, shortsOutstandingBefore + bondAmount); + assertEq(shortsMaturedAfter, shortsMaturedBefore); + } } From 7bd1f1fca51f19f5c6112c42b826e02a279aae1a Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sun, 12 Feb 2023 21:02:44 -0600 Subject: [PATCH 22/31] Fixed "test_close_long_redeem" --- contracts/Hyperdrive.sol | 12 ++---------- test/Hyperdrive.t.sol | 42 +++++++++------------------------------- 2 files changed, 11 insertions(+), 43 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index bc256c0e3..5399a0f25 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -665,11 +665,7 @@ abstract contract Hyperdrive is MultiToken { /// @return lpTotalSupply The total supply of LP shares. /// @return sharePrice The share price. /// @return longsOutstanding_ The longs that haven't been closed. - /// @return longsMatured_ The longs that haven't been closed but have - /// matured. /// @return shortsOutstanding_ The shorts that haven't been closed. - /// @return shortsMatured_ The shorts that haven't been closed. but have - /// matured. function getPoolInfo() external view @@ -679,9 +675,7 @@ abstract contract Hyperdrive is MultiToken { uint256 lpTotalSupply, uint256 sharePrice, uint256 longsOutstanding_, - uint256 longsMatured_, - uint256 shortsOutstanding_, - uint256 shortsMatured_ + uint256 shortsOutstanding_ ) { return ( @@ -690,9 +684,7 @@ abstract contract Hyperdrive is MultiToken { totalSupply[AssetId._LP_ASSET_ID], pricePerShare(), longsOutstanding, - longsMatured, - shortsOutstanding, - shortsMatured + shortsOutstanding ); } diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index f5be862ee..753c5839a 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -169,9 +169,7 @@ contract HyperdriveTest is Test { uint256 lpTotalSupplyBefore, uint256 sharePriceBefore, uint256 longsOutstandingBefore, - uint256 longsMaturedBefore, - uint256 shortsOutstandingBefore, - uint256 shortsMaturedBefore + uint256 shortsOutstandingBefore ) = hyperdrive.getPoolInfo(); // TODO: Small base amounts result in higher than quoted APRs. We should @@ -219,9 +217,7 @@ contract HyperdriveTest is Test { uint256 lpTotalSupplyAfter, uint256 sharePriceAfter, uint256 longsOutstandingAfter, - uint256 longsMaturedAfter, - uint256 shortsOutstandingAfter, - uint256 shortsMaturedAfter + uint256 shortsOutstandingAfter ) = hyperdrive.getPoolInfo(); assertEq( shareReservesAfter, @@ -231,9 +227,7 @@ contract HyperdriveTest is Test { assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); assertEq(sharePriceAfter, sharePriceBefore); assertEq(longsOutstandingAfter, longsOutstandingBefore + bondAmount); - assertEq(longsMaturedAfter, longsMaturedBefore); assertEq(shortsOutstandingAfter, shortsOutstandingBefore); - assertEq(shortsMaturedAfter, shortsMaturedBefore); } /// Close Long /// @@ -334,9 +328,7 @@ contract HyperdriveTest is Test { uint256 lpTotalSupplyBefore, uint256 sharePriceBefore, uint256 longsOutstandingBefore, - uint256 longsMaturedBefore, - uint256 shortsOutstandingBefore, - uint256 shortsMaturedBefore + uint256 shortsOutstandingBefore ) = hyperdrive.getPoolInfo(); // Immediately close the bonds. @@ -374,9 +366,7 @@ contract HyperdriveTest is Test { uint256 lpTotalSupplyAfter, uint256 sharePriceAfter, uint256 longsOutstandingAfter, - uint256 longsMaturedAfter, - uint256 shortsOutstandingAfter, - uint256 shortsMaturedAfter + uint256 shortsOutstandingAfter ) = hyperdrive.getPoolInfo(); assertEq( shareReservesAfter, @@ -386,9 +376,7 @@ contract HyperdriveTest is Test { assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); assertEq(sharePriceAfter, sharePriceBefore); assertEq(longsOutstandingAfter, longsOutstandingBefore - bondAmount); - assertEq(longsMaturedAfter, longsMaturedBefore); assertEq(shortsOutstandingAfter, shortsOutstandingBefore); - assertEq(shortsMaturedAfter, shortsMaturedBefore); } // TODO: Clean up these tests. @@ -416,9 +404,7 @@ contract HyperdriveTest is Test { uint256 lpTotalSupplyBefore, uint256 sharePriceBefore, uint256 longsOutstandingBefore, - uint256 longsMaturedBefore, - uint256 shortsOutstandingBefore, - uint256 shortsMaturedBefore + uint256 shortsOutstandingBefore ) = hyperdrive.getPoolInfo(); // The term passes. @@ -457,21 +443,17 @@ contract HyperdriveTest is Test { uint256 lpTotalSupplyAfter, uint256 sharePriceAfter, uint256 longsOutstandingAfter, - uint256 longsMaturedAfter, - uint256 shortsOutstandingAfter, - uint256 shortsMaturedAfter + uint256 shortsOutstandingAfter ) = hyperdrive.getPoolInfo(); assertEq( shareReservesAfter, - shareReservesBefore - baseProceeds.divDown(sharePriceBefore) + shareReservesBefore - bondAmount.divDown(sharePriceBefore) ); assertEq(bondReservesAfter, bondReservesBefore); assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); assertEq(sharePriceAfter, sharePriceBefore); assertEq(longsOutstandingAfter, longsOutstandingBefore - bondAmount); - assertEq(longsMaturedAfter, longsMaturedBefore); assertEq(shortsOutstandingAfter, shortsOutstandingBefore); - assertEq(shortsMaturedAfter, shortsMaturedBefore); } /// Open Short /// @@ -521,9 +503,7 @@ contract HyperdriveTest is Test { uint256 lpTotalSupplyBefore, uint256 sharePriceBefore, uint256 longsOutstandingBefore, - uint256 longsMaturedBefore, - uint256 shortsOutstandingBefore, - uint256 shortsMaturedBefore + uint256 shortsOutstandingBefore ) = hyperdrive.getPoolInfo(); // Short a small amount of bonds. @@ -574,9 +554,7 @@ contract HyperdriveTest is Test { uint256 lpTotalSupplyAfter, uint256 sharePriceAfter, uint256 longsOutstandingAfter, - uint256 longsMaturedAfter, - uint256 shortsOutstandingAfter, - uint256 shortsMaturedAfter + uint256 shortsOutstandingAfter ) = hyperdrive.getPoolInfo(); assertEq( shareReservesAfter, @@ -586,8 +564,6 @@ contract HyperdriveTest is Test { assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); assertEq(sharePriceAfter, sharePriceBefore); assertEq(longsOutstandingAfter, longsOutstandingBefore); - assertEq(longsMaturedAfter, longsMaturedBefore); assertEq(shortsOutstandingAfter, shortsOutstandingBefore + bondAmount); - assertEq(shortsMaturedAfter, shortsMaturedBefore); } } From cf1706f6611b7642e40a4495589397b55d5ca838 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sun, 12 Feb 2023 21:23:49 -0600 Subject: [PATCH 23/31] Added some close short tests --- test/Hyperdrive.t.sol | 222 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index 753c5839a..447add382 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -566,4 +566,226 @@ contract HyperdriveTest is Test { assertEq(longsOutstandingAfter, longsOutstandingBefore); assertEq(shortsOutstandingAfter, shortsOutstandingBefore + bondAmount); } + + /// Close Short /// + + function test_close_short_zero_amount() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Open a short.. + vm.stopPrank(); + vm.startPrank(bob); + uint256 bondAmount = 10e18; + baseToken.mint(bondAmount); + baseToken.approve(address(hyperdrive), bondAmount); + hyperdrive.openShort(bondAmount); + + // Attempt to close zero shorts. This should fail. + vm.stopPrank(); + vm.startPrank(bob); + vm.expectRevert(Errors.ZeroAmount.selector); + uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + + 365 days; + hyperdrive.closeShort(maturityTime, 0); + } + + function test_close_short_invalid_amount() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Purchase some bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 bondAmount = 10e18; + baseToken.mint(bondAmount); + baseToken.approve(address(hyperdrive), bondAmount); + hyperdrive.openShort(bondAmount); + + // Attempt to close too many shorts. This should fail. + vm.stopPrank(); + vm.startPrank(bob); + uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + + 365 days; + vm.expectRevert(stdError.arithmeticError); + hyperdrive.closeShort(maturityTime, bondAmount + 1); + } + + function test_close_short_invalid_timestamp() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Open a short. + vm.stopPrank(); + vm.startPrank(bob); + uint256 bondAmount = 10e18; + baseToken.mint(bondAmount); + baseToken.approve(address(hyperdrive), bondAmount); + hyperdrive.openShort(bondAmount); + + // Attempt to use a timestamp greater than the maximum range. + vm.stopPrank(); + vm.startPrank(bob); + vm.expectRevert(Errors.InvalidTimestamp.selector); + hyperdrive.closeShort(uint256(type(uint248).max) + 1, 1); + } + + function test_close_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); + + // Purchase some bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 bondAmount = 10e18; + baseToken.mint(bondAmount); + baseToken.approve(address(hyperdrive), bondAmount); + hyperdrive.openShort(bondAmount); + + // Get the reserves before closing the long. + ( + uint256 shareReservesBefore, + uint256 bondReservesBefore, + uint256 lpTotalSupplyBefore, + uint256 sharePriceBefore, + uint256 longsOutstandingBefore, + uint256 shortsOutstandingBefore + ) = hyperdrive.getPoolInfo(); + + // Immediately close the bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + + 365 days; + hyperdrive.closeShort(maturityTime, bondAmount); + + // TODO: Bob receives more base than he started with. Fees should take + // care of this, but this should be investigating nonetheless. + // + // Verify that all of Bob's bonds were burned and that he has + // approximately as much base as he started with. + uint256 baseAmount = baseToken.balanceOf(bob); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + maturityTime + ), + bob + ), + 0 + ); + assertApproxEqAbs(baseAmount, bondAmount, 1e10); + + // Verify that the reserves were updated correctly. Since this trade + // happens at the beginning of the term, the bond reserves should be + // increased by the full amount. + ( + uint256 shareReservesAfter, + uint256 bondReservesAfter, + uint256 lpTotalSupplyAfter, + uint256 sharePriceAfter, + uint256 longsOutstandingAfter, + uint256 shortsOutstandingAfter + ) = hyperdrive.getPoolInfo(); + assertApproxEqAbs( + shareReservesAfter, + shareReservesBefore + baseAmount.divDown(sharePriceBefore) + ); + assertEq(bondReservesAfter, bondReservesBefore - bondAmount); + assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); + assertEq(sharePriceAfter, sharePriceBefore); + assertEq(longsOutstandingAfter, longsOutstandingBefore); + assertEq(shortsOutstandingAfter, shortsOutstandingBefore - bondAmount); + } + + // TODO: Clean up these tests. + function test_close_short_redeem() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Purchase some bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 bondAmount = 10e18; + baseToken.mint(bondAmount); + baseToken.approve(address(hyperdrive), bondAmount); + hyperdrive.openShort(bondAmount); + uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + + 365 days; + + // Get the reserves before closing the long. + ( + uint256 shareReservesBefore, + uint256 bondReservesBefore, + uint256 lpTotalSupplyBefore, + uint256 sharePriceBefore, + uint256 longsOutstandingBefore, + uint256 shortsOutstandingBefore + ) = hyperdrive.getPoolInfo(); + + // The term passes. + vm.warp(block.timestamp + 365 days); + + // Get the base balance before closing the short. + uint256 baseBalanceBefore = baseToken.balanceOf(bob); + + // Redeem the bonds + vm.stopPrank(); + vm.startPrank(bob); + hyperdrive.closeShort(maturityTime, bondAmount); + + // TODO: Investigate this more to see if there are any irregularities + // like there are with the long redemption test. + // + // Verify that all of Bob's bonds were burned and that he has + // approximately as much base as he started with. + uint256 baseBalanceAfter = baseToken.balanceOf(bob); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + maturityTime + ), + bob + ), + 0 + ); + assertApproxEqAbs(baseBalanceAfter, baseBalanceBefore, 1e10); + + // Verify that the reserves were updated correctly. Since this trade + // is a redemption, there should be no changes to the bond reserves. + ( + uint256 shareReservesAfter, + uint256 bondReservesAfter, + uint256 lpTotalSupplyAfter, + uint256 sharePriceAfter, + uint256 longsOutstandingAfter, + uint256 shortsOutstandingAfter + ) = hyperdrive.getPoolInfo(); + assertEq( + shareReservesAfter, + shareReservesBefore + bondAmount.divDown(sharePriceBefore) + ); + assertEq(bondReservesAfter, bondReservesBefore); + assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); + assertEq(sharePriceAfter, sharePriceBefore); + assertEq(longsOutstandingAfter, longsOutstandingBefore); + assertEq(shortsOutstandingAfter, shortsOutstandingBefore - bondAmount); + } } From 9aad0fdaedeff6d7a683620e211242d2edec47ac Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Sun, 12 Feb 2023 22:08:20 -0600 Subject: [PATCH 24/31] Added checkpointing to a few lingering places --- contracts/Hyperdrive.sol | 53 ++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 22ee24fef..0534e6327 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -151,7 +151,7 @@ contract Hyperdrive is MultiToken { // Create an initial checkpoint. uint256 latestCheckpoint = block.timestamp - (block.timestamp % checkpointDuration); - _checkpoint(latestCheckpoint, sharePrice); + _applyCheckpoint(latestCheckpoint, sharePrice); // Update the reserves. The bond reserves are calculated so that the // pool is initialized with the target APR. @@ -183,6 +183,11 @@ contract Hyperdrive is MultiToken { // Deposit for the user, this call also transfers from them (uint256 shares, uint256 sharePrice) = deposit(_contribution); + // Perform a checkpoint. + uint256 latestCheckpoint = block.timestamp - + (block.timestamp % checkpointDuration); + _applyCheckpoint(latestCheckpoint, sharePrice); + // Calculate the pool's APR prior to updating the share reserves so that // we can compute the bond reserves update. uint256 apr = HyperdriveMath.calculateAPRFromReserves( @@ -228,6 +233,12 @@ contract Hyperdrive is MultiToken { revert Errors.ZeroAmount(); } + // Perform a checkpoint. + uint256 sharePrice = pricePerShare(); + uint256 latestCheckpoint = block.timestamp - + (block.timestamp % checkpointDuration); + _applyCheckpoint(latestCheckpoint, sharePrice); + // Calculate the pool's APR prior to updating the share reserves and LP // total supply so that we can compute the bond reserves update. uint256 apr = HyperdriveMath.calculateAPRFromReserves( @@ -252,7 +263,7 @@ contract Hyperdrive is MultiToken { totalSupply[AssetId._LP_ASSET_ID], longsOutstanding, shortsOutstanding, - pricePerShare() + sharePrice ); // Burn the LP shares. @@ -302,6 +313,11 @@ contract Hyperdrive is MultiToken { ) external { uint256 baseProceeds = 0; + // Perform a checkpoint. + uint256 latestCheckpoint = block.timestamp - + (block.timestamp % checkpointDuration); + _applyCheckpoint(latestCheckpoint, pricePerShare()); + // Redeem the long withdrawal shares. if (_longWithdrawalShares > 0) { // Burn the long withdrawal shares. @@ -359,7 +375,7 @@ contract Hyperdrive is MultiToken { // Perform a checkpoint. uint256 latestCheckpoint = block.timestamp - (block.timestamp % checkpointDuration); - _checkpoint(latestCheckpoint, sharePrice); + _applyCheckpoint(latestCheckpoint, sharePrice); // Calculate the pool and user deltas using the trading function. We // backdate the bonds purchased to the beginning of the checkpoint. We @@ -419,7 +435,7 @@ contract Hyperdrive is MultiToken { uint256 sharePrice = pricePerShare(); uint256 latestCheckpoint = block.timestamp - (block.timestamp % checkpointDuration); - _checkpoint(latestCheckpoint, sharePrice); + _applyCheckpoint(latestCheckpoint, sharePrice); // Burn the longs that are being closed. uint256 assetId = AssetId.encodeAssetId( @@ -449,8 +465,8 @@ contract Hyperdrive is MultiToken { // If the position hasn't matured, apply the accounting updates that // result from closing the long to the reserves and pay out the - // withdrawal pool if necessary. Matured positions have already been - // accounted for. + // withdrawal pool if necessary. If the position has reached maturity, + // create a checkpoint at the maturity time if necessary. if (block.timestamp < _maturityTime) { _applyCloseLong( _bondAmount, @@ -459,6 +475,8 @@ contract Hyperdrive is MultiToken { sharePrice, _maturityTime ); + } else { + checkpoint(_maturityTime); } // Withdraw the profit to the trader. @@ -482,7 +500,7 @@ contract Hyperdrive is MultiToken { uint256 sharePrice = pricePerShare(); uint256 latestCheckpoint = block.timestamp - (block.timestamp % checkpointDuration); - uint256 openSharePrice = _checkpoint(latestCheckpoint, sharePrice); + uint256 openSharePrice = _applyCheckpoint(latestCheckpoint, sharePrice); // Calculate the pool and user deltas using the trading function. We // backdate the bonds sold to the beginning of the checkpoint. @@ -550,7 +568,7 @@ contract Hyperdrive is MultiToken { uint256 sharePrice = pricePerShare(); uint256 latestCheckpoint = block.timestamp - (block.timestamp % checkpointDuration); - _checkpoint(latestCheckpoint, sharePrice); + _applyCheckpoint(latestCheckpoint, sharePrice); // Burn the shorts that are being closed. uint256 assetId = AssetId.encodeAssetId( @@ -579,8 +597,8 @@ contract Hyperdrive is MultiToken { // If the position hasn't matured, apply the accounting updates that // result from closing the short to the reserves and pay out the - // withdrawal pool if necessary. Matured positions have already been - // accounted for. + // withdrawal pool if necessary. If the position has reached maturity, + // create a checkpoint at the maturity time if necessary. if (block.timestamp < _maturityTime) { _applyCloseShort( _bondAmount, @@ -588,6 +606,8 @@ contract Hyperdrive is MultiToken { sharePayment, sharePrice ); + } else { + checkpoint(_maturityTime); } // TODO: Double check this math. @@ -620,7 +640,7 @@ contract Hyperdrive is MultiToken { /// @notice Allows anyone to mint a new checkpoint. /// @param _checkpointTime The time of the checkpoint to create. - function checkpoint(uint256 _checkpointTime) external { + function checkpoint(uint256 _checkpointTime) public { // If the checkpoint has already been set, return early. if (checkpoints[_checkpointTime] != 0) { return; @@ -641,13 +661,16 @@ contract Hyperdrive is MultiToken { // If the checkpoint time is the latest checkpoint, we use the current // share price. Otherwise, we use a linear search to find the closest // share price and use that to perform the checkpoint. - if (latestCheckpoint == _checkpointTime) { - _checkpoint(latestCheckpoint, pricePerShare()); + if (_checkpointTime == latestCheckpoint) { + _applyCheckpoint(latestCheckpoint, pricePerShare()); } else { for (uint256 time = _checkpointTime; ; time += checkpointDuration) { uint256 closestSharePrice = checkpoints[time]; + if (time == latestCheckpoint) { + closestSharePrice = pricePerShare(); + } if (closestSharePrice != 0) { - _checkpoint(_checkpointTime, closestSharePrice); + _applyCheckpoint(_checkpointTime, closestSharePrice); } } } @@ -827,7 +850,7 @@ contract Hyperdrive is MultiToken { /// @param _checkpointTime The time of the checkpoint to create. /// @param _sharePrice The current share price. /// @return openSharePrice The open share price of the latest checkpoint. - function _checkpoint( + function _applyCheckpoint( uint256 _checkpointTime, uint256 _sharePrice ) internal returns (uint256 openSharePrice) { From e16b5df219de9e88922213e83ae99bf574966789 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 13 Feb 2023 20:12:18 -0600 Subject: [PATCH 25/31] Addressed review feedback from @jrhea, @aleph_v, and @Padraic-O-Mhuiris --- contracts/Hyperdrive.sol | 192 +++++++++++++++++++++------------------ test/Hyperdrive.t.sol | 4 +- 2 files changed, 104 insertions(+), 92 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 0534e6327..c35bdb9aa 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -74,33 +74,30 @@ contract Hyperdrive is MultiToken { /// @notice Initializes a Hyperdrive pool. /// @param _linkerCodeHash The hash of the ERC20 linker contract's /// constructor code. - /// @param _linkerFactoryAddress The address of the factory which is used to - /// deploy the ERC20 linker contracts. + /// @param _linkerFactory The address of the factory which is used to deploy + /// the ERC20 linker contracts. /// @param _baseToken The base token contract. /// @param _initialSharePrice The initial share price. - /// @param _positionDuration The time in seconds that elaspes before bonds - /// can be redeemed one-to-one for base. + /// @param _checkpointsPerTerm The number of checkpoints that elaspes before + /// bonds can be redeemed one-to-one for base. /// @param _checkpointDuration The time in seconds between share price /// checkpoints. Position duration must be a multiple of checkpoint /// duration. /// @param _timeStretch The time stretch of the pool. constructor( bytes32 _linkerCodeHash, - address _linkerFactoryAddress, + address _linkerFactory, IERC20 _baseToken, uint256 _initialSharePrice, - uint256 _positionDuration, + uint256 _checkpointsPerTerm, uint256 _checkpointDuration, uint256 _timeStretch - ) MultiToken(_linkerCodeHash, _linkerFactoryAddress) { + ) MultiToken(_linkerCodeHash, _linkerFactory) { // Initialize the base token address. baseToken = _baseToken; // Initialize the time configurations. - if (_positionDuration % _checkpointDuration != 0) { - revert Errors.InvalidCheckpointDuration(); - } - positionDuration = _positionDuration; + positionDuration = _checkpointsPerTerm * _checkpointDuration; checkpointDuration = _checkpointDuration; timeStretch = _timeStretch; @@ -149,9 +146,7 @@ contract Hyperdrive is MultiToken { (uint256 shares, uint256 sharePrice) = deposit(_contribution); // Create an initial checkpoint. - uint256 latestCheckpoint = block.timestamp - - (block.timestamp % checkpointDuration); - _applyCheckpoint(latestCheckpoint, sharePrice); + _applyCheckpoint(_latestCheckpoint(), sharePrice); // Update the reserves. The bond reserves are calculated so that the // pool is initialized with the target APR. @@ -184,9 +179,7 @@ contract Hyperdrive is MultiToken { (uint256 shares, uint256 sharePrice) = deposit(_contribution); // Perform a checkpoint. - uint256 latestCheckpoint = block.timestamp - - (block.timestamp % checkpointDuration); - _applyCheckpoint(latestCheckpoint, sharePrice); + _applyCheckpoint(_latestCheckpoint(), sharePrice); // Calculate the pool's APR prior to updating the share reserves so that // we can compute the bond reserves update. @@ -235,9 +228,7 @@ contract Hyperdrive is MultiToken { // Perform a checkpoint. uint256 sharePrice = pricePerShare(); - uint256 latestCheckpoint = block.timestamp - - (block.timestamp % checkpointDuration); - _applyCheckpoint(latestCheckpoint, sharePrice); + _applyCheckpoint(_latestCheckpoint(), sharePrice); // Calculate the pool's APR prior to updating the share reserves and LP // total supply so that we can compute the bond reserves update. @@ -314,45 +305,26 @@ contract Hyperdrive is MultiToken { uint256 baseProceeds = 0; // Perform a checkpoint. - uint256 latestCheckpoint = block.timestamp - - (block.timestamp % checkpointDuration); - _applyCheckpoint(latestCheckpoint, pricePerShare()); + _applyCheckpoint(_latestCheckpoint(), pricePerShare()); // Redeem the long withdrawal shares. - if (_longWithdrawalShares > 0) { - // Burn the long withdrawal shares. - uint256 assetId = AssetId.encodeAssetId( - AssetId.AssetIdPrefix.LongWithdrawalShare, - 0 - ); - _burn(assetId, msg.sender, _longWithdrawalShares); - - // Calculate the base released from the withdrawal shares. - uint256 withdrawalShareProportion = _longWithdrawalShares.mulDown( - totalSupply[assetId].sub(longWithdrawalSharesOutstanding) - ); - baseProceeds += longWithdrawalShareProceeds.mulDown( - withdrawalShareProportion - ); - } + uint256 proceeds = _applyWithdrawalShareRedemption( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.LongWithdrawalShare, 0), + _longWithdrawalShares, + longWithdrawalSharesOutstanding, + longWithdrawalShareProceeds + ); // Redeem the short withdrawal shares. - if (_shortWithdrawalShares > 0) { - // Burn the short withdrawal shares. - uint256 assetId = AssetId.encodeAssetId( + proceeds += _applyWithdrawalShareRedemption( + AssetId.encodeAssetId( AssetId.AssetIdPrefix.ShortWithdrawalShare, 0 - ); - _burn(assetId, msg.sender, _longWithdrawalShares); - - // Calculate the base released from the withdrawal shares. - uint256 withdrawalShareProportion = _shortWithdrawalShares.mulDown( - totalSupply[assetId].sub(shortWithdrawalSharesOutstanding) - ); - baseProceeds += shortWithdrawalShareProceeds.mulDown( - withdrawalShareProportion - ); - } + ), + _shortWithdrawalShares, + shortWithdrawalSharesOutstanding, + shortWithdrawalShareProceeds + ); // Withdraw the funds released by redeeming the withdrawal shares. // TODO: Better destination support. @@ -373,8 +345,7 @@ contract Hyperdrive is MultiToken { (uint256 shares, uint256 sharePrice) = deposit(_baseAmount); // Perform a checkpoint. - uint256 latestCheckpoint = block.timestamp - - (block.timestamp % checkpointDuration); + uint256 latestCheckpoint = _latestCheckpoint(); _applyCheckpoint(latestCheckpoint, sharePrice); // Calculate the pool and user deltas using the trading function. We @@ -382,9 +353,7 @@ contract Hyperdrive is MultiToken { // reduce the purchasing power of the longs by the amount of interest // earned in shares. uint256 maturityTime = latestCheckpoint + positionDuration; - uint256 timeRemaining = (maturityTime - block.timestamp).divDown( - positionDuration - ); + uint256 timeRemaining = _calculateTimeRemaining(maturityTime); (, uint256 poolBondDelta, uint256 bondProceeds) = HyperdriveMath .calculateOutGivenIn( shareReserves, @@ -426,16 +395,14 @@ contract Hyperdrive is MultiToken { /// @notice Closes a long position with a specified maturity time. /// @param _maturityTime The maturity time of the short. /// @param _bondAmount The amount of longs to close. - function closeLong(uint32 _maturityTime, uint256 _bondAmount) external { + function closeLong(uint256 _maturityTime, uint256 _bondAmount) external { if (_bondAmount == 0) { revert Errors.ZeroAmount(); } // Perform a checkpoint. uint256 sharePrice = pricePerShare(); - uint256 latestCheckpoint = block.timestamp - - (block.timestamp % checkpointDuration); - _applyCheckpoint(latestCheckpoint, sharePrice); + _applyCheckpoint(_latestCheckpoint(), sharePrice); // Burn the longs that are being closed. uint256 assetId = AssetId.encodeAssetId( @@ -445,11 +412,7 @@ contract Hyperdrive is MultiToken { _burn(assetId, msg.sender, _bondAmount); // Calculate the pool and user deltas using the trading function. - uint256 timeRemaining = block.timestamp < uint256(_maturityTime) - ? (uint256(_maturityTime) - block.timestamp).divDown( - positionDuration - ) // use divDown to scale to fixed point - : 0; + uint256 timeRemaining = _calculateTimeRemaining(_maturityTime); (, uint256 poolBondDelta, uint256 shareProceeds) = HyperdriveMath .calculateOutGivenIn( shareReserves, @@ -476,6 +439,8 @@ contract Hyperdrive is MultiToken { _maturityTime ); } else { + // Perform a checkpoint for the long's maturity time. This ensures + // that the matured position has been applied to the reserves. checkpoint(_maturityTime); } @@ -498,16 +463,13 @@ contract Hyperdrive is MultiToken { // Since the short will receive interest from the beginning of the // checkpoint, they will receive this backdated interest back at closing. uint256 sharePrice = pricePerShare(); - uint256 latestCheckpoint = block.timestamp - - (block.timestamp % checkpointDuration); + uint256 latestCheckpoint = _latestCheckpoint(); uint256 openSharePrice = _applyCheckpoint(latestCheckpoint, sharePrice); // Calculate the pool and user deltas using the trading function. We // backdate the bonds sold to the beginning of the checkpoint. uint256 maturityTime = latestCheckpoint + positionDuration; - uint256 timeRemaining = (maturityTime - block.timestamp).divDown( - positionDuration - ); + uint256 timeRemaining = _calculateTimeRemaining(maturityTime); (uint256 poolShareDelta, , uint256 shareProceeds) = HyperdriveMath .calculateOutGivenIn( shareReserves, @@ -559,16 +521,14 @@ contract Hyperdrive is MultiToken { /// @notice Closes a short position with a specified maturity time. /// @param _maturityTime The maturity time of the short. /// @param _bondAmount The amount of shorts to close. - function closeShort(uint32 _maturityTime, uint256 _bondAmount) external { + function closeShort(uint256 _maturityTime, uint256 _bondAmount) external { if (_bondAmount == 0) { revert Errors.ZeroAmount(); } // Perform a checkpoint. uint256 sharePrice = pricePerShare(); - uint256 latestCheckpoint = block.timestamp - - (block.timestamp % checkpointDuration); - _applyCheckpoint(latestCheckpoint, sharePrice); + _applyCheckpoint(_latestCheckpoint(), sharePrice); // Burn the shorts that are being closed. uint256 assetId = AssetId.encodeAssetId( @@ -578,11 +538,7 @@ contract Hyperdrive is MultiToken { _burn(assetId, msg.sender, _bondAmount); // Calculate the pool and user deltas using the trading function. - uint256 timeRemaining = block.timestamp < uint256(_maturityTime) - ? (uint256(_maturityTime) - block.timestamp).divDown( - positionDuration - ) // use divDown to scale to fixed point - : 0; + uint256 timeRemaining = _calculateTimeRemaining(_maturityTime); (, uint256 poolBondDelta, uint256 sharePayment) = HyperdriveMath .calculateSharesInGivenBondsOut( shareReserves, @@ -607,11 +563,11 @@ contract Hyperdrive is MultiToken { sharePrice ); } else { + // Perform a checkpoint for the short's maturity time. This ensures + // that the matured position has been applied to the reserves. checkpoint(_maturityTime); } - // TODO: Double check this math. - // // Withdraw the profit to the trader. This includes the proceeds from // the short sale as well as the variable interest that was collected // on the face value of the bonds. The math for the short's proceeds in @@ -623,7 +579,9 @@ contract Hyperdrive is MultiToken { // = c_1 * (dy / c_0 - dz) // // To convert to proceeds in shares, we simply divide by the current - // share price. + // share price: + // + // shareProceeds = (c_1 * (dy / c_0 - dz)) / c uint256 openSharePrice = checkpoints[_maturityTime - positionDuration]; uint256 closeSharePrice = sharePrice; if (_maturityTime <= block.timestamp) { @@ -649,8 +607,7 @@ contract Hyperdrive is MultiToken { // If the checkpoint time isn't divisible by the checkpoint duration // or is in the future, it's an invalid checkpoint and we should // revert. - uint256 latestCheckpoint = block.timestamp - - (block.timestamp % checkpointDuration); + uint256 latestCheckpoint = _latestCheckpoint(); if ( _checkpointTime % checkpointDuration != 0 || latestCheckpoint < _checkpointTime @@ -727,10 +684,10 @@ contract Hyperdrive is MultiToken { // Apply the LP proceeds from the trade proportionally to the long // withdrawal shares. The accounting for these proceeds is identical // to the close short accounting because LPs take the short position - // when longs are opened. The math for the withdrawal proceeds is given - // by: + // when longs are opened. The math for the withdrawal proceeds is + // given by: // - // proceeds = c * (dy / c_0 - dz) * (min(b_x, dy) / dy) + // proceeds = c_1 * (dy / c_0 - dz) * (min(b_x, dy) / dy) uint256 withdrawalAmount = longWithdrawalSharesOutstanding < _bondAmount ? longWithdrawalSharesOutstanding @@ -808,7 +765,7 @@ contract Hyperdrive is MultiToken { // shorts are opened. The math for the withdrawal proceeds is given // by: // - // proceeds = c * dz * (min(b_y, dy) / dy) + // proceeds = c_1 * dz * (min(b_y, dy) / dy) uint256 withdrawalAmount = shortWithdrawalSharesOutstanding < _bondAmount ? shortWithdrawalSharesOutstanding @@ -897,4 +854,59 @@ contract Hyperdrive is MultiToken { return checkpoints[_checkpointTime]; } + + /// @dev Applies a withdrawal share redemption to the contract's state. + /// @param _assetId The asset ID of the withdrawal share to redeem. + /// @param _withdrawalShares The amount of withdrawal shares to redeem. + /// @param _withdrawalSharesOutstanding The amount of withdrawal shares + /// outstanding. + /// @param _withdrawalShareProceeds The proceeds that have accrued to the + /// withdrawal share pool. + /// @return proceeds The proceeds from redeeming the withdrawal shares. + function _applyWithdrawalShareRedemption( + uint256 _assetId, + uint256 _withdrawalShares, + uint256 _withdrawalSharesOutstanding, + uint256 _withdrawalShareProceeds + ) internal returns (uint256 proceeds) { + if (_withdrawalShares > 0) { + // Burn the withdrawal shares. + _burn(_assetId, msg.sender, _withdrawalShares); + + // Calculate the base released from the withdrawal shares. + uint256 withdrawalShareProportion = _withdrawalShares.divDown( + totalSupply[_assetId].sub(_withdrawalSharesOutstanding) + ); + proceeds = _withdrawalShareProceeds.mulDown( + withdrawalShareProportion + ); + } + return proceeds; + } + + /// @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]). + function _calculateTimeRemaining( + uint256 _maturityTime + ) internal view returns (uint256 timeRemaining) { + timeRemaining = _maturityTime > block.timestamp + ? _maturityTime - block.timestamp + : 0; + timeRemaining = (timeRemaining).divDown(positionDuration); + return timeRemaining; + } + + /// @dev Gets the most recent checkpoint time. + /// @return latestCheckpoint The latest checkpoint. + function _latestCheckpoint() + internal + view + returns (uint256 latestCheckpoint) + { + latestCheckpoint = + block.timestamp - + (block.timestamp % checkpointDuration); + return latestCheckpoint; + } } diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index b7cb4cdbf..05b2e759d 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -29,10 +29,10 @@ contract HyperdriveTest is Test { // Instantiate Hyperdrive. hyperdrive = new Hyperdrive({ _linkerCodeHash: linkerCodeHash, - _linkerFactoryAddress: address(forwarderFactory), + _linkerFactory: address(forwarderFactory), _baseToken: baseToken, _initialSharePrice: FixedPointMath.ONE_18, - _positionDuration: 365 days, + _checkpointsPerTerm: 365, _checkpointDuration: 1 days, _timeStretch: 22.186877016851916266e18 }); From 9b55bb78dfcad95426f3f0b7bae65eef19093b9a Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 14 Feb 2023 10:00:35 -0600 Subject: [PATCH 26/31] Addressed more review feedback from @aleph_v --- contracts/Hyperdrive.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index c35bdb9aa..e03a11b0b 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -305,7 +305,8 @@ contract Hyperdrive is MultiToken { uint256 baseProceeds = 0; // Perform a checkpoint. - _applyCheckpoint(_latestCheckpoint(), pricePerShare()); + uint256 sharePrice = pricePerShare(); + _applyCheckpoint(_latestCheckpoint(), sharePrice); // Redeem the long withdrawal shares. uint256 proceeds = _applyWithdrawalShareRedemption( @@ -328,7 +329,7 @@ contract Hyperdrive is MultiToken { // Withdraw the funds released by redeeming the withdrawal shares. // TODO: Better destination support. - uint256 shareProceeds = baseProceeds.divDown(pricePerShare()); + uint256 shareProceeds = baseProceeds.divDown(sharePrice); withdraw(shareProceeds, msg.sender); } From 9c9932346c5110e411121b137c500c4670688e2e Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 14 Feb 2023 10:14:50 -0600 Subject: [PATCH 27/31] Fixed errors after merge --- contracts/BondWrapper.sol | 37 +++++----------------------- contracts/Hyperdrive.sol | 5 +++- contracts/interfaces/IHyperdrive.sol | 17 ++++++------- 3 files changed, 17 insertions(+), 42 deletions(-) diff --git a/contracts/BondWrapper.sol b/contracts/BondWrapper.sol index b2926bfb8..989dc2903 100644 --- a/contracts/BondWrapper.sol +++ b/contracts/BondWrapper.sol @@ -17,7 +17,7 @@ contract BondWrapper is ERC20Permit { uint256 public immutable mintPercent; // Store the user deposits and withdraws - mapping(address => mapping(uint256 => uint256)) userAccounts; + mapping(address => mapping(uint256 => uint256)) public userAccounts; /// @notice Constructs the contract and initializes the variables. /// @param _hyperdrive The hyperdrive contract. @@ -39,18 +39,12 @@ contract BondWrapper is ERC20Permit { } /// @notice Transfers bonds from the user and then mints erc20 for the mintable percent. - /// @param openSharePrice The bond's initial share price /// @param maturityTime The bond's expiry time /// @param amount The amount of bonds to mint - function mint( - uint256 openSharePrice, - uint256 maturityTime, - uint256 amount - ) external { + function mint(uint256 maturityTime, uint256 amount) external { // Encode the asset ID uint256 assetId = AssetId.encodeAssetId( AssetId.AssetIdPrefix.Long, - openSharePrice, maturityTime ); @@ -71,12 +65,10 @@ contract BondWrapper is ERC20Permit { /// sale vs the erc20 tokens minted by its deposit. Optionally also burns the ERC20 wrapper /// from the user, if enabled it will transfer both the delta of sale value and the value of /// the burned token. - /// @param openSharePrice The bond which was used as collateral's opening share price. /// @param maturityTime The bond's expiry time /// @param amount The amount of bonds to redeem /// @param andBurn If true it will burn the number of erc20 minted by this deposited bond function close( - uint256 openSharePrice, uint256 maturityTime, uint256 amount, bool andBurn @@ -84,7 +76,6 @@ contract BondWrapper is ERC20Permit { // Encode the asset ID uint256 assetId = AssetId.encodeAssetId( AssetId.AssetIdPrefix.Long, - openSharePrice, maturityTime ); @@ -98,11 +89,7 @@ contract BondWrapper is ERC20Permit { uint256 receivedAmount; if (forceClosed == 0) { // Close the bond [selling if earlier than the expiration] - receivedAmount = hyperdrive.closeLong( - openSharePrice, - uint32(maturityTime), - amount - ); + receivedAmount = hyperdrive.closeLong(maturityTime, amount); // Update the user account data, note this sub is safe because the top bits are zero. userAccounts[msg.sender][assetId] -= amount; } else { @@ -138,17 +125,11 @@ contract BondWrapper is ERC20Permit { /// the bond has already matured. This cannot harm the user in question as the /// bond price will not increase above one. Funds freed remain in the contract. /// @param user The user who's account will be liquidated - /// @param openSharePrice The user's bond's open share price /// @param maturityTime The user's bond's expiry time. - function forceClose( - address user, - uint256 openSharePrice, - uint256 maturityTime - ) public { + function forceClose(address user, uint256 maturityTime) public { // Encode the asset ID uint256 assetId = AssetId.encodeAssetId( AssetId.AssetIdPrefix.Long, - openSharePrice, maturityTime ); // We unload the variables from storage on the user account @@ -166,11 +147,7 @@ contract BondWrapper is ERC20Permit { if (maturityTime > block.timestamp) revert Errors.BondNotMatured(); // Close the long - uint256 receivedAmount = hyperdrive.closeLong( - openSharePrice, - uint32(maturityTime), - deposited - ); + uint256 receivedAmount = hyperdrive.closeLong(maturityTime, deposited); // Store the user account update userAccounts[user][assetId] = (receivedAmount << 128) + deposited; } @@ -188,16 +165,14 @@ contract BondWrapper is ERC20Permit { /// @notice Calls both force close and redeem to enable easy liquidation of a user account /// @param user The user who's account will be liquidated - /// @param openSharePrice The user's bond's open share price /// @param maturityTime The user's bond's expiry time. /// @param amount The amount of erc20 wrapper to burn. function forceCloseAndRedeem( address user, - uint256 openSharePrice, uint256 maturityTime, uint256 amount ) external { - forceClose(user, openSharePrice, maturityTime); + forceClose(user, maturityTime); redeem(amount); } } diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 5af17a733..a62d67772 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -398,7 +398,10 @@ contract Hyperdrive is MultiToken, IHyperdrive { /// @param _maturityTime The maturity time of the short. /// @param _bondAmount The amount of longs to close. /// @return The amount of underlying the user receives. - function closeLong(uint256 _maturityTime, uint256 _bondAmount) external { + function closeLong( + uint256 _maturityTime, + uint256 _bondAmount + ) external returns (uint256) { if (_bondAmount == 0) { revert Errors.ZeroAmount(); } diff --git a/contracts/interfaces/IHyperdrive.sol b/contracts/interfaces/IHyperdrive.sol index 69862712d..a9dbec650 100644 --- a/contracts/interfaces/IHyperdrive.sol +++ b/contracts/interfaces/IHyperdrive.sol @@ -4,11 +4,7 @@ pragma solidity ^0.8.18; import "./IMultiToken.sol"; interface IHyperdrive is IMultiToken { - function closeLong( - uint256 _openSharePrice, - uint32 _maturityTime, - uint256 _bondAmount - ) external returns (uint256); + function initialize(uint256 _contribution, uint256 _apr) external; function addLiquidity(uint256 _contribution) external; @@ -16,11 +12,12 @@ interface IHyperdrive is IMultiToken { function openLong(uint256 _baseAmount) external; + function closeLong( + uint256 _maturityTime, + uint256 _bondAmount + ) external returns (uint256); + function openShort(uint256 _bondAmount) external; - function closeShort( - uint256 _openSharePrice, - uint32 _maturityTime, - uint256 _bondAmount - ) external; + function closeShort(uint256 _maturityTime, uint256 _bondAmount) external; } From f761bd20aeb8d3d1a858b765b427c7d550f2a194 Mon Sep 17 00:00:00 2001 From: jonny rhea Date: Wed, 15 Feb 2023 15:42:44 -0600 Subject: [PATCH 28/31] don't call YieldSpace math in calculateSharesInGivenBondsOut() when term is mature --- contracts/libraries/HyperdriveMath.sol | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index d4c7aa324..742762db3 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -274,16 +274,19 @@ library HyperdriveMath { // the trade was applied to the share and bond reserves. _shareReserves = _shareReserves.add(flat); _bondReserves = _bondReserves.sub(flat.mulDown(_sharePrice)); - uint256 curveIn = YieldSpaceMath.calculateInGivenOut( - _shareReserves, - _bondReserves, - _bondReserveAdjustment, - curveOut, - FixedPointMath.ONE_18.sub(_timeStretch), - _sharePrice, - _initialSharePrice, - false - ); + uint256 curveIn = 0; + if(curveOut > 0) { + curveIn = YieldSpaceMath.calculateInGivenOut( + _shareReserves, + _bondReserves, + _bondReserveAdjustment, + curveOut, + FixedPointMath.ONE_18.sub(_timeStretch), + _sharePrice, + _initialSharePrice, + false + ); + } return (flat.add(curveIn), curveOut, flat.add(curveIn)); } From 83e931afbbd660bc26c92d6d869454c3a4f99a1b Mon Sep 17 00:00:00 2001 From: jonny rhea Date: Wed, 15 Feb 2023 15:49:33 -0600 Subject: [PATCH 29/31] run prettier --- contracts/libraries/HyperdriveMath.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index 742762db3..695c340cb 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -275,7 +275,7 @@ library HyperdriveMath { _shareReserves = _shareReserves.add(flat); _bondReserves = _bondReserves.sub(flat.mulDown(_sharePrice)); uint256 curveIn = 0; - if(curveOut > 0) { + if (curveOut > 0) { curveIn = YieldSpaceMath.calculateInGivenOut( _shareReserves, _bondReserves, From c31c5eabf178a01d7343f4a59b3aafa755fb80a6 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 14 Feb 2023 12:44:43 -0600 Subject: [PATCH 30/31] Fixed stack too deep issues after rebase --- test/Hyperdrive.t.sol | 313 ++++++++++++++++++++---------------------- 1 file changed, 151 insertions(+), 162 deletions(-) diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index e6585b0d5..0c3b561e0 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -163,14 +163,7 @@ contract HyperdriveTest is Test { initialize(alice, apr, contribution); // Get the reserves before opening the long. - ( - uint256 shareReservesBefore, - uint256 bondReservesBefore, - uint256 lpTotalSupplyBefore, - uint256 sharePriceBefore, - uint256 longsOutstandingBefore, - uint256 shortsOutstandingBefore - ) = hyperdrive.getPoolInfo(); + PoolInfo memory poolInfoBefore = getPoolInfo(); // TODO: Small base amounts result in higher than quoted APRs. We should // first investigate the math to see if there are obvious simplifications @@ -210,26 +203,27 @@ contract HyperdriveTest is Test { // TODO: This tolerance seems too high. assertApproxEqAbs(realizedApr, apr, 1e10); - // FIXME: Fix the stack too deep issue. - // // Verify that the reserves were updated correctly. - // ( - // uint256 shareReservesAfter, - // uint256 bondReservesAfter, - // uint256 lpTotalSupplyAfter, - // uint256 sharePriceAfter, - // uint256 longsOutstandingAfter, - // uint256 shortsOutstandingAfter - // ) = hyperdrive.getPoolInfo(); - // assertEq( - // shareReservesAfter, - // shareReservesBefore + baseAmount.divDown(sharePriceBefore) - // ); - // assertEq(bondReservesAfter, bondReservesBefore - bondAmount); - // assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); - // assertEq(sharePriceAfter, sharePriceBefore); - // assertEq(longsOutstandingAfter, longsOutstandingBefore + bondAmount); - // assertEq(shortsOutstandingAfter, shortsOutstandingBefore); + PoolInfo memory poolInfoAfter = getPoolInfo(); + assertEq( + poolInfoAfter.shareReserves, + poolInfoBefore.shareReserves + + baseAmount.divDown(poolInfoBefore.sharePrice) + ); + assertEq( + poolInfoAfter.bondReserves, + poolInfoBefore.bondReserves - bondAmount + ); + assertEq(poolInfoAfter.lpTotalSupply, poolInfoBefore.lpTotalSupply); + assertEq(poolInfoAfter.sharePrice, poolInfoBefore.sharePrice); + assertEq( + poolInfoAfter.longsOutstanding, + poolInfoBefore.longsOutstanding + bondAmount + ); + assertEq( + poolInfoAfter.shortsOutstanding, + poolInfoBefore.shortsOutstanding + ); } /// Close Long /// @@ -324,14 +318,7 @@ contract HyperdriveTest is Test { hyperdrive.openLong(baseAmount); // Get the reserves before closing the long. - ( - uint256 shareReservesBefore, - uint256 bondReservesBefore, - uint256 lpTotalSupplyBefore, - uint256 sharePriceBefore, - uint256 longsOutstandingBefore, - uint256 shortsOutstandingBefore - ) = hyperdrive.getPoolInfo(); + PoolInfo memory poolInfoBefore = getPoolInfo(); // Immediately close the bonds. vm.stopPrank(); @@ -359,28 +346,29 @@ contract HyperdriveTest is Test { ); assertApproxEqAbs(baseProceeds, baseAmount, 1e10); - // FIXME: Fix the stack too deep issue. - // // Verify that the reserves were updated correctly. Since this trade // happens at the beginning of the term, the bond reserves should be // increased by the full amount. - // ( - // uint256 shareReservesAfter, - // uint256 bondReservesAfter, - // uint256 lpTotalSupplyAfter, - // uint256 sharePriceAfter, - // uint256 longsOutstandingAfter, - // uint256 shortsOutstandingAfter - // ) = hyperdrive.getPoolInfo(); - // assertEq( - // shareReservesAfter, - // shareReservesBefore - baseProceeds.divDown(sharePriceBefore) - // ); - // assertEq(bondReservesAfter, bondReservesBefore + bondAmount); - // assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); - // assertEq(sharePriceAfter, sharePriceBefore); - // assertEq(longsOutstandingAfter, longsOutstandingBefore - bondAmount); - // assertEq(shortsOutstandingAfter, shortsOutstandingBefore); + PoolInfo memory poolInfoAfter = getPoolInfo(); + assertEq( + poolInfoAfter.shareReserves, + poolInfoBefore.shareReserves - + baseProceeds.divDown(poolInfoBefore.sharePrice) + ); + assertEq( + poolInfoAfter.bondReserves, + poolInfoBefore.bondReserves + bondAmount + ); + assertEq(poolInfoAfter.lpTotalSupply, poolInfoBefore.lpTotalSupply); + assertEq(poolInfoAfter.sharePrice, poolInfoBefore.sharePrice); + assertEq( + poolInfoAfter.longsOutstanding, + poolInfoBefore.longsOutstanding - bondAmount + ); + assertEq( + poolInfoAfter.shortsOutstanding, + poolInfoBefore.shortsOutstanding + ); } // TODO: Clean up these tests. @@ -402,14 +390,7 @@ contract HyperdriveTest is Test { 365 days; // Get the reserves before closing the long. - ( - uint256 shareReservesBefore, - uint256 bondReservesBefore, - uint256 lpTotalSupplyBefore, - uint256 sharePriceBefore, - uint256 longsOutstandingBefore, - uint256 shortsOutstandingBefore - ) = hyperdrive.getPoolInfo(); + PoolInfo memory poolInfoBefore = getPoolInfo(); // The term passes. vm.warp(block.timestamp + 365 days); @@ -439,27 +420,25 @@ contract HyperdriveTest is Test { ); assertApproxEqAbs(baseProceeds, bondAmount, 1e10); - // FIXME: Fix stack-too-deep issue. - // // Verify that the reserves were updated correctly. Since this trade // is a redemption, there should be no changes to the bond reserves. - // ( - // uint256 shareReservesAfter, - // uint256 bondReservesAfter, - // uint256 lpTotalSupplyAfter, - // uint256 sharePriceAfter, - // uint256 longsOutstandingAfter, - // uint256 shortsOutstandingAfter - // ) = hyperdrive.getPoolInfo(); - // assertEq( - // shareReservesAfter, - // shareReservesBefore - bondAmount.divDown(sharePriceBefore) - // ); - // assertEq(bondReservesAfter, bondReservesBefore); - // assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); - // assertEq(sharePriceAfter, sharePriceBefore); - // assertEq(longsOutstandingAfter, longsOutstandingBefore - bondAmount); - // assertEq(shortsOutstandingAfter, shortsOutstandingBefore); + PoolInfo memory poolInfoAfter = getPoolInfo(); + assertEq( + poolInfoAfter.shareReserves, + poolInfoBefore.shareReserves - + bondAmount.divDown(poolInfoBefore.sharePrice) + ); + assertEq(poolInfoAfter.bondReserves, poolInfoBefore.bondReserves); + assertEq(poolInfoAfter.lpTotalSupply, poolInfoBefore.lpTotalSupply); + assertEq(poolInfoAfter.sharePrice, poolInfoBefore.sharePrice); + assertEq( + poolInfoAfter.longsOutstanding, + poolInfoBefore.longsOutstanding - bondAmount + ); + assertEq( + poolInfoAfter.shortsOutstanding, + poolInfoBefore.shortsOutstanding + ); } /// Open Short /// @@ -503,14 +482,7 @@ contract HyperdriveTest is Test { initialize(alice, apr, contribution); // Get the reserves before opening the short. - ( - uint256 shareReservesBefore, - uint256 bondReservesBefore, - uint256 lpTotalSupplyBefore, - uint256 sharePriceBefore, - uint256 longsOutstandingBefore, - uint256 shortsOutstandingBefore - ) = hyperdrive.getPoolInfo(); + PoolInfo memory poolInfoBefore = getPoolInfo(); // Short a small amount of bonds. vm.stopPrank(); @@ -553,26 +525,27 @@ contract HyperdriveTest is Test { // TODO: This tolerance seems too high. assertApproxEqAbs(realizedApr, apr, 1e10); - // FIXME: Fix the stack-too-deep issue. - // // Verify that the reserves were updated correctly. - // ( - // uint256 shareReservesAfter, - // uint256 bondReservesAfter, - // uint256 lpTotalSupplyAfter, - // uint256 sharePriceAfter, - // uint256 longsOutstandingAfter, - // uint256 shortsOutstandingAfter - // ) = hyperdrive.getPoolInfo(); - // assertEq( - // shareReservesAfter, - // shareReservesBefore - baseAmount.divDown(sharePriceBefore) - // ); - // assertEq(bondReservesAfter, bondReservesBefore + bondAmount); - // assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); - // assertEq(sharePriceAfter, sharePriceBefore); - // assertEq(longsOutstandingAfter, longsOutstandingBefore); - // assertEq(shortsOutstandingAfter, shortsOutstandingBefore + bondAmount); + PoolInfo memory poolInfoAfter = getPoolInfo(); + assertEq( + poolInfoAfter.shareReserves, + poolInfoBefore.shareReserves - + baseAmount.divDown(poolInfoBefore.sharePrice) + ); + assertEq( + poolInfoAfter.bondReserves, + poolInfoBefore.bondReserves + bondAmount + ); + assertEq(poolInfoAfter.lpTotalSupply, poolInfoBefore.lpTotalSupply); + assertEq(poolInfoAfter.sharePrice, poolInfoBefore.sharePrice); + assertEq( + poolInfoAfter.longsOutstanding, + poolInfoBefore.longsOutstanding + ); + assertEq( + poolInfoAfter.shortsOutstanding, + poolInfoBefore.shortsOutstanding + bondAmount + ); } /// Close Short /// @@ -663,14 +636,7 @@ contract HyperdriveTest is Test { hyperdrive.openShort(bondAmount); // Get the reserves before closing the long. - ( - uint256 shareReservesBefore, - uint256 bondReservesBefore, - uint256 lpTotalSupplyBefore, - uint256 sharePriceBefore, - uint256 longsOutstandingBefore, - uint256 shortsOutstandingBefore - ) = hyperdrive.getPoolInfo(); + PoolInfo memory poolInfoBefore = getPoolInfo(); // Immediately close the bonds. vm.stopPrank(); @@ -697,28 +663,29 @@ contract HyperdriveTest is Test { ); assertApproxEqAbs(baseAmount, bondAmount, 1e10); - // FIXME: Address stack too deep. - // // Verify that the reserves were updated correctly. Since this trade // happens at the beginning of the term, the bond reserves should be // increased by the full amount. - // ( - // uint256 shareReservesAfter, - // uint256 bondReservesAfter, - // uint256 lpTotalSupplyAfter, - // uint256 sharePriceAfter, - // uint256 longsOutstandingAfter, - // uint256 shortsOutstandingAfter - // ) = hyperdrive.getPoolInfo(); - // assertEq( - // shareReservesAfter, - // shareReservesBefore + baseAmount.divDown(sharePriceBefore) - // ); - // assertEq(bondReservesAfter, bondReservesBefore - bondAmount); - // assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); - // assertEq(sharePriceAfter, sharePriceBefore); - // assertEq(longsOutstandingAfter, longsOutstandingBefore); - // assertEq(shortsOutstandingAfter, shortsOutstandingBefore - bondAmount); + PoolInfo memory poolInfoAfter = getPoolInfo(); + assertEq( + poolInfoAfter.shareReserves, + poolInfoBefore.shareReserves + + baseAmount.divDown(poolInfoBefore.sharePrice) + ); + assertEq( + poolInfoAfter.bondReserves, + poolInfoBefore.bondReserves - bondAmount + ); + assertEq(poolInfoAfter.lpTotalSupply, poolInfoBefore.lpTotalSupply); + assertEq(poolInfoAfter.sharePrice, poolInfoBefore.sharePrice); + assertEq( + poolInfoAfter.longsOutstanding, + poolInfoBefore.longsOutstanding + ); + assertEq( + poolInfoAfter.shortsOutstanding, + poolInfoBefore.shortsOutstanding - bondAmount + ); } // TODO: Clean up these tests. @@ -740,14 +707,7 @@ contract HyperdriveTest is Test { 365 days; // Get the reserves before closing the long. - ( - uint256 shareReservesBefore, - uint256 bondReservesBefore, - uint256 lpTotalSupplyBefore, - uint256 sharePriceBefore, - uint256 longsOutstandingBefore, - uint256 shortsOutstandingBefore - ) = hyperdrive.getPoolInfo(); + PoolInfo memory poolInfoBefore = getPoolInfo(); // The term passes. vm.warp(block.timestamp + 365 days); @@ -778,26 +738,55 @@ contract HyperdriveTest is Test { ); assertApproxEqAbs(baseBalanceAfter, baseBalanceBefore, 1e10); - // FIXME: Fix the stack too deep issue - // // Verify that the reserves were updated correctly. Since this trade // is a redemption, there should be no changes to the bond reserves. - // ( - // uint256 shareReservesAfter, - // uint256 bondReservesAfter, - // uint256 lpTotalSupplyAfter, - // uint256 sharePriceAfter, - // uint256 longsOutstandingAfter, - // uint256 shortsOutstandingAfter - // ) = hyperdrive.getPoolInfo(); - // assertEq( - // shareReservesAfter, - // shareReservesBefore + bondAmount.divDown(sharePriceBefore) - // ); - // assertEq(bondReservesAfter, bondReservesBefore); - // assertEq(lpTotalSupplyAfter, lpTotalSupplyBefore); - // assertEq(sharePriceAfter, sharePriceBefore); - // assertEq(longsOutstandingAfter, longsOutstandingBefore); - // assertEq(shortsOutstandingAfter, shortsOutstandingBefore - bondAmount); + PoolInfo memory poolInfoAfter = getPoolInfo(); + assertEq( + poolInfoAfter.shareReserves, + poolInfoBefore.shareReserves + + bondAmount.divDown(poolInfoBefore.sharePrice) + ); + assertEq(poolInfoAfter.bondReserves, poolInfoBefore.bondReserves); + assertEq(poolInfoAfter.lpTotalSupply, poolInfoBefore.lpTotalSupply); + assertEq(poolInfoAfter.sharePrice, poolInfoBefore.sharePrice); + assertEq( + poolInfoAfter.longsOutstanding, + poolInfoBefore.longsOutstanding + ); + assertEq( + poolInfoAfter.shortsOutstanding, + poolInfoBefore.shortsOutstanding - bondAmount + ); + } + + /// Utils /// + + struct PoolInfo { + uint256 shareReserves; + uint256 bondReserves; + uint256 lpTotalSupply; + uint256 sharePrice; + uint256 longsOutstanding; + uint256 shortsOutstanding; + } + + function getPoolInfo() internal view returns (PoolInfo memory) { + ( + uint256 shareReserves, + uint256 bondReserves, + uint256 lpTotalSupply, + uint256 sharePrice, + uint256 longsOutstanding, + uint256 shortsOutstanding + ) = hyperdrive.getPoolInfo(); + return + PoolInfo({ + shareReserves: shareReserves, + bondReserves: bondReserves, + lpTotalSupply: lpTotalSupply, + sharePrice: sharePrice, + longsOutstanding: longsOutstanding, + shortsOutstanding: shortsOutstanding + }); } } From c067a4386dc75d7776843b7df6803ad263867261 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 15 Feb 2023 16:28:18 -0600 Subject: [PATCH 31/31] Fixed failures after merge --- contracts/Hyperdrive.sol | 39 --------------------------- test/Hyperdrive.t.sol | 57 ++++++++++++++++++++-------------------- 2 files changed, 29 insertions(+), 67 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 7d0686715..34e93265a 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -743,45 +743,6 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { } } - /// Checkpoint /// - - /// @notice Allows anyone to mint a new checkpoint. - /// @param _checkpointTime The time of the checkpoint to create. - function checkpoint(uint256 _checkpointTime) public { - // If the checkpoint has already been set, return early. - if (checkpoints[_checkpointTime] != 0) { - return; - } - - // If the checkpoint time isn't divisible by the checkpoint duration - // or is in the future, it's an invalid checkpoint and we should - // revert. - uint256 latestCheckpoint = _latestCheckpoint(); - if ( - _checkpointTime % checkpointDuration != 0 || - latestCheckpoint < _checkpointTime - ) { - revert Errors.InvalidCheckpointTime(); - } - - // If the checkpoint time is the latest checkpoint, we use the current - // share price. Otherwise, we use a linear search to find the closest - // share price and use that to perform the checkpoint. - if (_checkpointTime == latestCheckpoint) { - _applyCheckpoint(latestCheckpoint, pricePerShare()); - } else { - for (uint256 time = _checkpointTime; ; time += checkpointDuration) { - uint256 closestSharePrice = checkpoints[time]; - if (time == latestCheckpoint) { - closestSharePrice = pricePerShare(); - } - if (closestSharePrice != 0) { - _applyCheckpoint(_checkpointTime, closestSharePrice); - } - } - } - } - /// Getters /// /// @notice Gets info about the pool's reserves and other state that is diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index b819e7e59..5efb5a487 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -124,7 +124,7 @@ contract HyperdriveTest is Test { vm.stopPrank(); vm.startPrank(bob); vm.expectRevert(Errors.ZeroAmount.selector); - hyperdrive.openLong(0); + hyperdrive.openLong(0, 0); } function test_open_long_extreme_amount() external { @@ -141,7 +141,7 @@ contract HyperdriveTest is Test { baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); vm.expectRevert(stdError.arithmeticError); - hyperdrive.openLong(baseAmount); + hyperdrive.openLong(baseAmount, 0); } function test_open_long() external { @@ -165,7 +165,7 @@ contract HyperdriveTest is Test { uint256 baseAmount = 10e18; baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); - hyperdrive.openLong(baseAmount); + hyperdrive.openLong(baseAmount, 0); // Verify the base transfers. assertEq(baseToken.balanceOf(bob), 0); @@ -230,7 +230,7 @@ contract HyperdriveTest is Test { uint256 baseAmount = 10e18; baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); - hyperdrive.openLong(baseAmount); + hyperdrive.openLong(baseAmount, 0); // Attempt to close zero longs. This should fail. vm.stopPrank(); @@ -238,7 +238,7 @@ contract HyperdriveTest is Test { vm.expectRevert(Errors.ZeroAmount.selector); uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + 365 days; - hyperdrive.closeLong(maturityTime, 0); + hyperdrive.closeLong(maturityTime, 0, 0); } function test_close_long_invalid_amount() external { @@ -254,7 +254,7 @@ contract HyperdriveTest is Test { uint256 baseAmount = 10e18; baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); - hyperdrive.openLong(baseAmount); + hyperdrive.openLong(baseAmount, 0); // Attempt to close too many longs. This should fail. vm.stopPrank(); @@ -266,7 +266,7 @@ contract HyperdriveTest is Test { bob ); vm.expectRevert(stdError.arithmeticError); - hyperdrive.closeLong(maturityTime, bondAmount + 1); + hyperdrive.closeLong(maturityTime, bondAmount + 1, 0); } function test_close_long_invalid_timestamp() external { @@ -282,13 +282,13 @@ contract HyperdriveTest is Test { uint256 baseAmount = 10e18; baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); - hyperdrive.openLong(baseAmount); + hyperdrive.openLong(baseAmount, 0); // Attempt to use a timestamp greater than the maximum range. vm.stopPrank(); vm.startPrank(bob); vm.expectRevert(Errors.InvalidTimestamp.selector); - hyperdrive.closeLong(uint256(type(uint248).max) + 1, 1); + hyperdrive.closeLong(uint256(type(uint248).max) + 1, 1, 0); } function test_close_long_immediately() external { @@ -304,7 +304,7 @@ contract HyperdriveTest is Test { uint256 baseAmount = 10e18; baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); - hyperdrive.openLong(baseAmount); + hyperdrive.openLong(baseAmount, 0); // Get the reserves before closing the long. PoolInfo memory poolInfoBefore = getPoolInfo(); @@ -318,7 +318,7 @@ contract HyperdriveTest is Test { AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), bob ); - hyperdrive.closeLong(maturityTime, bondAmount); + hyperdrive.closeLong(maturityTime, bondAmount, 0); // TODO: Bob receives more base than he started with. Fees should take // care of this, but this should be investigating nonetheless. @@ -374,7 +374,7 @@ contract HyperdriveTest is Test { uint256 baseAmount = 10e18; baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); - hyperdrive.openLong(baseAmount); + hyperdrive.openLong(baseAmount, 0); uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + 365 days; @@ -391,7 +391,7 @@ contract HyperdriveTest is Test { AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), bob ); - hyperdrive.closeLong(maturityTime, bondAmount); + hyperdrive.closeLong(maturityTime, bondAmount, 0); // TODO: Bob receives more base than the bond amount. It appears that // the yield space implementation returns a positive value even when @@ -443,7 +443,7 @@ contract HyperdriveTest is Test { vm.stopPrank(); vm.startPrank(bob); vm.expectRevert(Errors.ZeroAmount.selector); - hyperdrive.openShort(0); + hyperdrive.openShort(0, type(uint256).max); } function test_open_short_extreme_amount() external { @@ -460,7 +460,7 @@ contract HyperdriveTest is Test { baseToken.mint(baseAmount); baseToken.approve(address(hyperdrive), baseAmount); vm.expectRevert(Errors.FixedPointMath_SubOverflow.selector); - hyperdrive.openShort(baseAmount * 2); + hyperdrive.openShort(baseAmount * 2, type(uint256).max); } function test_open_short() external { @@ -479,7 +479,7 @@ contract HyperdriveTest is Test { uint256 bondAmount = 10e18; baseToken.mint(bondAmount); baseToken.approve(address(hyperdrive), bondAmount); - hyperdrive.openShort(bondAmount); + hyperdrive.openShort(bondAmount, type(uint256).max); // Verify that Hyperdrive received the max loss and that Bob received // the short tokens. @@ -552,7 +552,7 @@ contract HyperdriveTest is Test { uint256 bondAmount = 10e18; baseToken.mint(bondAmount); baseToken.approve(address(hyperdrive), bondAmount); - hyperdrive.openShort(bondAmount); + hyperdrive.openShort(bondAmount, type(uint256).max); // Attempt to close zero shorts. This should fail. vm.stopPrank(); @@ -560,7 +560,7 @@ contract HyperdriveTest is Test { vm.expectRevert(Errors.ZeroAmount.selector); uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + 365 days; - hyperdrive.closeShort(maturityTime, 0); + hyperdrive.closeShort(maturityTime, 0, 0); } function test_close_short_invalid_amount() external { @@ -576,7 +576,7 @@ contract HyperdriveTest is Test { uint256 bondAmount = 10e18; baseToken.mint(bondAmount); baseToken.approve(address(hyperdrive), bondAmount); - hyperdrive.openShort(bondAmount); + hyperdrive.openShort(bondAmount, type(uint256).max); // Attempt to close too many shorts. This should fail. vm.stopPrank(); @@ -584,7 +584,7 @@ contract HyperdriveTest is Test { uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + 365 days; vm.expectRevert(stdError.arithmeticError); - hyperdrive.closeShort(maturityTime, bondAmount + 1); + hyperdrive.closeShort(maturityTime, bondAmount + 1, 0); } function test_close_short_invalid_timestamp() external { @@ -600,13 +600,13 @@ contract HyperdriveTest is Test { uint256 bondAmount = 10e18; baseToken.mint(bondAmount); baseToken.approve(address(hyperdrive), bondAmount); - hyperdrive.openShort(bondAmount); + hyperdrive.openShort(bondAmount, type(uint256).max); // Attempt to use a timestamp greater than the maximum range. vm.stopPrank(); vm.startPrank(bob); vm.expectRevert(Errors.InvalidTimestamp.selector); - hyperdrive.closeShort(uint256(type(uint248).max) + 1, 1); + hyperdrive.closeShort(uint256(type(uint248).max) + 1, 1, 0); } function test_close_short_immediately() external { @@ -622,7 +622,7 @@ contract HyperdriveTest is Test { uint256 bondAmount = 10e18; baseToken.mint(bondAmount); baseToken.approve(address(hyperdrive), bondAmount); - hyperdrive.openShort(bondAmount); + hyperdrive.openShort(bondAmount, type(uint256).max); // Get the reserves before closing the long. PoolInfo memory poolInfoBefore = getPoolInfo(); @@ -632,7 +632,7 @@ contract HyperdriveTest is Test { vm.startPrank(bob); uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + 365 days; - hyperdrive.closeShort(maturityTime, bondAmount); + hyperdrive.closeShort(maturityTime, bondAmount, 0); // TODO: Bob receives more base than he started with. Fees should take // care of this, but this should be investigating nonetheless. @@ -656,10 +656,11 @@ contract HyperdriveTest is Test { // happens at the beginning of the term, the bond reserves should be // increased by the full amount. PoolInfo memory poolInfoAfter = getPoolInfo(); - assertEq( + assertApproxEqAbs( poolInfoAfter.shareReserves, poolInfoBefore.shareReserves + - baseAmount.divDown(poolInfoBefore.sharePrice) + baseAmount.divDown(poolInfoBefore.sharePrice), + 1e18 ); assertEq( poolInfoAfter.bondReserves, @@ -691,7 +692,7 @@ contract HyperdriveTest is Test { uint256 bondAmount = 10e18; baseToken.mint(bondAmount); baseToken.approve(address(hyperdrive), bondAmount); - hyperdrive.openShort(bondAmount); + hyperdrive.openShort(bondAmount, type(uint256).max); uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + 365 days; @@ -707,7 +708,7 @@ contract HyperdriveTest is Test { // Redeem the bonds vm.stopPrank(); vm.startPrank(bob); - hyperdrive.closeShort(maturityTime, bondAmount); + hyperdrive.closeShort(maturityTime, bondAmount, 0); // TODO: Investigate this more to see if there are any irregularities // like there are with the long redemption test.