From 66edc7f1a20c50e3532815028342998c94f9c5a2 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 19 Nov 2024 21:41:40 -1000 Subject: [PATCH 01/45] Adds an `openPair` function --- contracts/src/interfaces/IHyperdrive.sol | 15 ++ .../src/interfaces/IHyperdriveEvents.sol | 14 ++ contracts/src/internal/HyperdriveBase.sol | 42 ++-- contracts/src/internal/HyperdriveLP.sol | 6 +- contracts/src/internal/HyperdriveLong.sol | 3 +- contracts/src/internal/HyperdrivePair.sol | 186 ++++++++++++++++++ contracts/src/internal/HyperdriveShort.sol | 2 +- contracts/test/MockERC4626Hyperdrive.sol | 2 +- 8 files changed, 253 insertions(+), 17 deletions(-) create mode 100644 contracts/src/internal/HyperdrivePair.sol diff --git a/contracts/src/interfaces/IHyperdrive.sol b/contracts/src/interfaces/IHyperdrive.sol index 8c13180ef..c9e897d8d 100644 --- a/contracts/src/interfaces/IHyperdrive.sol +++ b/contracts/src/interfaces/IHyperdrive.sol @@ -200,6 +200,21 @@ interface IHyperdrive is bytes extraData; } + struct PairOptions { + /// @dev The address that receives the long proceeds from a pair action. + address longDestination; + /// @dev The address that receives the short proceeds from a pair action. + address shortDestination; + /// @dev A boolean indicating that the trade or LP action should be + /// settled in base if true and in the yield source shares if false. + bool asBase; + /// @dev Additional data that can be used to implement custom logic in + /// implementation contracts. By convention, the last 32 bytes of + /// extra data are ignored by instances and "passed through" to the + /// event. This can be used to pass metadata through transactions. + bytes extraData; + } + /// Errors /// /// @notice Thrown when the inputs to a batch transfer don't match in diff --git a/contracts/src/interfaces/IHyperdriveEvents.sol b/contracts/src/interfaces/IHyperdriveEvents.sol index 4a8b8ca13..f563b4b01 100644 --- a/contracts/src/interfaces/IHyperdriveEvents.sol +++ b/contracts/src/interfaces/IHyperdriveEvents.sol @@ -102,6 +102,20 @@ interface IHyperdriveEvents is IMultiTokenEvents { bytes extraData ); + /// @notice Emitted when a pair of long and short positions are opened. + event OpenPair( + address indexed longTrader, + address indexed shortTrader, + uint256 indexed maturityTime, + uint256 longAssetId, + uint256 shortAssetId, + uint256 amount, + uint256 vaultSharePrice, + bool asBase, + uint256 bondAmount, + bytes extraData + ); + /// @notice Emitted when a checkpoint is created. event CreateCheckpoint( uint256 indexed checkpointTime, diff --git a/contracts/src/internal/HyperdriveBase.sol b/contracts/src/internal/HyperdriveBase.sol index 352db9998..9458b14d5 100644 --- a/contracts/src/internal/HyperdriveBase.sol +++ b/contracts/src/internal/HyperdriveBase.sol @@ -29,16 +29,20 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { /// @dev Process a deposit in either base or vault shares. /// @param _amount The amount of capital to deposit. The units of this /// quantity are either base or vault shares, depending on the value - /// of `_options.asBase`. - /// @param _options The options that configure how the deposit is - /// settled. In particular, the currency used in the deposit is - /// specified here. Aside from those options, yield sources can - /// choose to implement additional options. + /// of `_asBase`. + /// @param _asBase A flag indicating if the deposit should be made in base + /// or in vault shares. + /// @param _extraData Additional data that can be used to implement custom + /// logic in implementation contracts. By convention, the last 32 + /// bytes of extra data are ignored by instances and "passed through" + /// to the event. This can be used to pass metadata through + /// transactions. /// @return sharesMinted The shares created by this deposit. /// @return vaultSharePrice The vault share price. function _deposit( uint256 _amount, - IHyperdrive.Options calldata _options + bool _asBase, + bytes calldata _extraData ) internal returns (uint256 sharesMinted, uint256 vaultSharePrice) { // WARN: This logic doesn't account for slippage in the conversion // from base to shares. If deposits to the yield source incur @@ -50,19 +54,16 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { // Deposit with either base or shares depending on the provided options. uint256 refund; - if (_options.asBase) { + if (_asBase) { // Process the deposit in base. - (sharesMinted, refund) = _depositWithBase( - _amount, - _options.extraData - ); + (sharesMinted, refund) = _depositWithBase(_amount, _extraData); } else { // The refund is equal to the full message value since ETH will // never be a shares asset. refund = msg.value; // Process the deposit in shares. - _depositWithShares(_amount, _options.extraData); + _depositWithShares(_amount, _extraData); } // Calculate the vault share price. @@ -198,6 +199,23 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { } } + /// @dev A yield source dependent check that verifies that the provided + /// pair options are valid. The default check is that the destinations + /// are non-zero to prevent users from accidentally transferring funds + /// to the zero address. Custom integrations can override this to + /// implement additional checks. + /// @param _options The provided options for the transaction. + function _checkPairOptions( + IHyperdrive.PairOptions calldata _options + ) internal pure virtual { + if ( + _options.longDestination == address(0) || + _options.shortDestination == address(0) + ) { + revert IHyperdrive.RestrictedZeroAddress(); + } + } + /// @dev Convert an amount of vault shares to an amount of base. /// @param _shareAmount The vault shares amount. /// @return baseAmount The base amount. diff --git a/contracts/src/internal/HyperdriveLP.sol b/contracts/src/internal/HyperdriveLP.sol index e36d93492..15bfe3456 100644 --- a/contracts/src/internal/HyperdriveLP.sol +++ b/contracts/src/internal/HyperdriveLP.sol @@ -55,7 +55,8 @@ abstract contract HyperdriveLP is // their contribution was worth. (uint256 shareContribution, uint256 vaultSharePrice) = _deposit( _contribution, - _options + _options.asBase, + _options.extraData ); // Ensure that the contribution is large enough to set aside the minimum @@ -210,7 +211,8 @@ abstract contract HyperdriveLP is // Deposit for the user, this call also transfers from them (uint256 shareContribution, uint256 vaultSharePrice) = _deposit( _contribution, - _options + _options.asBase, + _options.extraData ); // Perform a checkpoint. diff --git a/contracts/src/internal/HyperdriveLong.sol b/contracts/src/internal/HyperdriveLong.sol index 81119d828..c385d6154 100644 --- a/contracts/src/internal/HyperdriveLong.sol +++ b/contracts/src/internal/HyperdriveLong.sol @@ -55,7 +55,8 @@ abstract contract HyperdriveLong is IHyperdriveEvents, HyperdriveLP { // Deposit the user's input amount. (uint256 sharesDeposited, uint256 vaultSharePrice) = _deposit( _amount, - _options + _options.asBase, + _options.extraData ); // Enforce the minimum user outputs and the minimum vault share price. diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol new file mode 100644 index 000000000..28a063f69 --- /dev/null +++ b/contracts/src/internal/HyperdrivePair.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.22; + +import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; +import { IHyperdriveEvents } from "../interfaces/IHyperdriveEvents.sol"; +import { AssetId } from "../libraries/AssetId.sol"; +import { FixedPointMath, ONE } from "../libraries/FixedPointMath.sol"; +import { HyperdriveMath } from "../libraries/HyperdriveMath.sol"; +import { LPMath } from "../libraries/LPMath.sol"; +import { SafeCast } from "../libraries/SafeCast.sol"; +import { HyperdriveBase } from "./HyperdriveLP.sol"; +import { HyperdriveMultiToken } from "./HyperdriveMultiToken.sol"; + +/// @author DELV +/// @title HyperdriveLong +/// @notice Implements the long accounting for Hyperdrive. +/// @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. +abstract contract HyperdrivePair is + IHyperdriveEvents, + HyperdriveBase, + HyperdriveMultiToken +{ + using FixedPointMath for uint256; + using FixedPointMath for int256; + using SafeCast for uint256; + using SafeCast for int256; + + // FIXME: Which (if any) slippage guards should this have? `minVaultSharePrice` + // seems reasonable. + // + // FIXME: This function should do the following: + // + // 1. [x] Accept a deposit of `_amount` with `_options.asBase` configuring the + // deposit from the msg.sender. + // 2. [x] Given the amount of shares from the deposit, convert the shares + // amount to base. + // 3. [x] Mint longs and shorts with bond amounts equal to the base amount + // deposited. + // 4. [x] Update the pool's accounting to reflect the new longs and shorts. + // 5. [x] Send the longs and shorts to the appropriate destination addresses. + // 6. [x] Emit an event. + // 7. [ ] Compute a governance fee for both positions. + // + /// @dev Opens a pair of long and short positions that directly match each + /// other. The amount of long and short positions that are created is + /// equal to the base value of the deposit. These positions are sent to + /// the provided destinations. + /// @param _amount The amount of capital provided to open the long. The + /// units of this quantity are either base or vault shares, depending + /// on the value of `_options.asBase`. + /// @param _options The pair options that configure how the trade is settled. + /// @return maturityTime The maturity time of the new long and short positions. + /// @return bondAmount The bond amount of the new long and short positoins. + function _openPair( + uint256 _amount, + uint256 _minVaultSharePrice, + IHyperdrive.PairOptions calldata _options + ) + internal + nonReentrant + isNotPaused + returns (uint256 maturityTime, uint256 bondAmount) + { + // Check that the message value is valid. + _checkMessageValue(); + + // Check that the provided options are valid. + _checkPairOptions(_options); + + // Deposit the user's input amount. The amount of base deposited is + // equal to the amount of bonds that will be minted. + (uint256 sharesDeposited, uint256 vaultSharePrice) = _deposit( + _amount, + _options.asBase, + _options.extraData + ); + bondAmount = sharesDeposited.mulDown(vaultSharePrice); + + // Enforce the minimum vault share price. + if (vaultSharePrice < _minVaultSharePrice) { + revert IHyperdrive.MinimumSharePrice(); + } + + // Perform a checkpoint. + uint256 latestCheckpoint = _latestCheckpoint(); + _applyCheckpoint( + latestCheckpoint, + vaultSharePrice, + LPMath.SHARE_PROCEEDS_MAX_ITERATIONS, + true + ); + + // Apply the state changes caused by creating the pair. + maturityTime = latestCheckpoint + _positionDuration; + _applyCreatePair(maturityTime, sharesDeposited, bondAmount); + + // Mint bonds equal in value to the base deposited. + uint256 longAssetId = AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + maturityTime + ); + uint256 shortAssetId = AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + maturityTime + ); + _mint(longAssetId, _options.longDestination, bondAmount); + _mint(shortAssetId, _options.shortDestination, bondAmount); + + // Emit an OpenPair event. + emit OpenPair( + _options.longDestination, + _options.shortDestination, + maturityTime, + longAssetId, + shortAssetId, + _amount, + vaultSharePrice, + _options.asBase, + bondAmount, + _options.extraData + ); + + return (maturityTime, bondAmount); + } + + // FIXME + // function _redeemPair( + // uint256 _amount, + // IHyperdrive.Options calldata _options + // ) internal returns (uint256 maturityTime, uint256 longAmount, uint256 shortAmount) { + // // FIXME + // } + + /// @dev Applies state changes to create a pair of matched long and short + /// positions. This operation leaves the pool's solvency and idle + /// capital unchanged because the positions fully net out. Specifically: + /// + /// - Share reserves, share adjustments, and bond reserves remain + /// constant since the provided capital backs the positions directly. + /// - Solvency remains constant because the net effect of matching long + /// and short positions is neutral. + /// - Idle capital is unaffected since no excess funds are added or + /// removed during this process. + /// + /// Therefore: + /// + /// - Solvency checks are unnecessary. + /// - Idle capital does not need to be redistributed to LPs. + /// @param _maturityTime The maturity time of the pair of long and short + /// positions + /// @param _sharesDeposited The amount of shares deposited. + /// @param _bondAmount The amount of bonds created. + function _applyCreatePair( + uint256 _maturityTime, + uint256 _sharesDeposited, + uint256 _bondAmount + ) internal { + // Update the average maturity time of longs and short positions and the + // amount of long and short positions outstanding. Everything else + // remains constant. + _marketState.longAverageMaturityTime = uint256( + _marketState.longAverageMaturityTime + ) + .updateWeightedAverage( + _marketState.longsOutstanding, + _maturityTime * ONE, // scale up to fixed point scale + _bondAmount, + true + ) + .toUint128(); + _marketState.shortAverageMaturityTime = uint256( + _marketState.shortAverageMaturityTime + ) + .updateWeightedAverage( + _marketState.shortsOutstanding, + _maturityTime * ONE, // scale up to fixed point scale + _bondAmount, + true + ) + .toUint128(); + _marketState.longsOutstanding += _bondAmount.toUint128(); + _marketState.shortsOutstanding += _bondAmount.toUint128(); + } +} diff --git a/contracts/src/internal/HyperdriveShort.sol b/contracts/src/internal/HyperdriveShort.sol index 34fa2aac9..ba8532105 100644 --- a/contracts/src/internal/HyperdriveShort.sol +++ b/contracts/src/internal/HyperdriveShort.sol @@ -129,7 +129,7 @@ abstract contract HyperdriveShort is IHyperdriveEvents, HyperdriveLP { if (_maxDeposit < deposit) { revert IHyperdrive.OutputLimit(); } - _deposit(deposit, _options); + _deposit(deposit, _options.asBase, _options.extraData); // Apply the state updates caused by opening the short. // Note: Updating the state using the result using the diff --git a/contracts/test/MockERC4626Hyperdrive.sol b/contracts/test/MockERC4626Hyperdrive.sol index 7864479f5..abc63b7e7 100644 --- a/contracts/test/MockERC4626Hyperdrive.sol +++ b/contracts/test/MockERC4626Hyperdrive.sol @@ -34,7 +34,7 @@ contract MockERC4626Hyperdrive is ERC4626Hyperdrive { uint256 _amount, IHyperdrive.Options calldata _options ) public returns (uint256 sharesMinted, uint256 vaultSharePrice) { - return _deposit(_amount, _options); + return _deposit(_amount, _options.asBase, _options.extraData); } function withdraw( From 2614a15445f1ef6dc61657fde70f7cf3322dc85a Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 19 Nov 2024 21:45:45 -1000 Subject: [PATCH 02/45] Removes some outdated comments --- contracts/src/internal/HyperdrivePair.sol | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index 28a063f69..460fad798 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -27,21 +27,8 @@ abstract contract HyperdrivePair is using SafeCast for uint256; using SafeCast for int256; - // FIXME: Which (if any) slippage guards should this have? `minVaultSharePrice` - // seems reasonable. - // - // FIXME: This function should do the following: - // - // 1. [x] Accept a deposit of `_amount` with `_options.asBase` configuring the - // deposit from the msg.sender. - // 2. [x] Given the amount of shares from the deposit, convert the shares - // amount to base. - // 3. [x] Mint longs and shorts with bond amounts equal to the base amount - // deposited. - // 4. [x] Update the pool's accounting to reflect the new longs and shorts. - // 5. [x] Send the longs and shorts to the appropriate destination addresses. - // 6. [x] Emit an event. - // 7. [ ] Compute a governance fee for both positions. + // FIXME: Add in a governance fee that is taken from the deposit amount and + // reduces the bond amount earned. // /// @dev Opens a pair of long and short positions that directly match each /// other. The amount of long and short positions that are created is From 8a557f6a5c9b5ff8b58cd5b7764cfed99cdbbcd9 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 22 Nov 2024 16:53:54 -1000 Subject: [PATCH 03/45] Added governance fees to the mint function --- .../src/interfaces/IHyperdriveEvents.sol | 4 +-- contracts/src/internal/HyperdrivePair.sol | 34 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/contracts/src/interfaces/IHyperdriveEvents.sol b/contracts/src/interfaces/IHyperdriveEvents.sol index f563b4b01..26f5d5e34 100644 --- a/contracts/src/interfaces/IHyperdriveEvents.sol +++ b/contracts/src/interfaces/IHyperdriveEvents.sol @@ -102,8 +102,8 @@ interface IHyperdriveEvents is IMultiTokenEvents { bytes extraData ); - /// @notice Emitted when a pair of long and short positions are opened. - event OpenPair( + /// @notice Emitted when a pair of long and short positions are minted. + event Mint( address indexed longTrader, address indexed shortTrader, uint256 indexed maturityTime, diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index 460fad798..44b5e1dbc 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -27,10 +27,7 @@ abstract contract HyperdrivePair is using SafeCast for uint256; using SafeCast for int256; - // FIXME: Add in a governance fee that is taken from the deposit amount and - // reduces the bond amount earned. - // - /// @dev Opens a pair of long and short positions that directly match each + /// @dev Mints a pair of long and short positions that directly match each /// other. The amount of long and short positions that are created is /// equal to the base value of the deposit. These positions are sent to /// the provided destinations. @@ -40,7 +37,7 @@ abstract contract HyperdrivePair is /// @param _options The pair options that configure how the trade is settled. /// @return maturityTime The maturity time of the new long and short positions. /// @return bondAmount The bond amount of the new long and short positoins. - function _openPair( + function _mint( uint256 _amount, uint256 _minVaultSharePrice, IHyperdrive.PairOptions calldata _options @@ -56,14 +53,22 @@ abstract contract HyperdrivePair is // Check that the provided options are valid. _checkPairOptions(_options); - // Deposit the user's input amount. The amount of base deposited is - // equal to the amount of bonds that will be minted. + // Deposit the user's input amount. (uint256 sharesDeposited, uint256 vaultSharePrice) = _deposit( _amount, _options.asBase, _options.extraData ); - bondAmount = sharesDeposited.mulDown(vaultSharePrice); + + // The governance fee is twice the governance fee paid on the flat fee + // since a long and short are both minted. + uint256 governanceFee = 2 * + sharesDeposited.mulDown(_flatFee).mulDown(_governanceLPFee); + _governanceFeesAccrued += governanceFee; + + // The amount of bonds that will be minted is equal to the amount of + // base deposited minus the governance fee. + bondAmount = (sharesDeposited - governanceFee).mulDown(vaultSharePrice); // Enforce the minimum vault share price. if (vaultSharePrice < _minVaultSharePrice) { @@ -81,7 +86,7 @@ abstract contract HyperdrivePair is // Apply the state changes caused by creating the pair. maturityTime = latestCheckpoint + _positionDuration; - _applyCreatePair(maturityTime, sharesDeposited, bondAmount); + _applyMint(maturityTime, bondAmount); // Mint bonds equal in value to the base deposited. uint256 longAssetId = AssetId.encodeAssetId( @@ -95,8 +100,8 @@ abstract contract HyperdrivePair is _mint(longAssetId, _options.longDestination, bondAmount); _mint(shortAssetId, _options.shortDestination, bondAmount); - // Emit an OpenPair event. - emit OpenPair( + // Emit an Mint event. + emit Mint( _options.longDestination, _options.shortDestination, maturityTime, @@ -137,13 +142,8 @@ abstract contract HyperdrivePair is /// - Idle capital does not need to be redistributed to LPs. /// @param _maturityTime The maturity time of the pair of long and short /// positions - /// @param _sharesDeposited The amount of shares deposited. /// @param _bondAmount The amount of bonds created. - function _applyCreatePair( - uint256 _maturityTime, - uint256 _sharesDeposited, - uint256 _bondAmount - ) internal { + function _applyMint(uint256 _maturityTime, uint256 _bondAmount) internal { // Update the average maturity time of longs and short positions and the // amount of long and short positions outstanding. Everything else // remains constant. From 4f754606ecd85658dd6d81c6eb4b9bae72155eb4 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 25 Nov 2024 18:18:10 -0800 Subject: [PATCH 04/45] Updated the implementation of `HyperdrivePair` --- contracts/src/internal/HyperdrivePair.sol | 74 ++++++++++++++++++++--- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index 44b5e1dbc..665bdf863 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -27,6 +27,9 @@ abstract contract HyperdrivePair is using SafeCast for uint256; using SafeCast for int256; + // FIXME: Is there anything weird here about needing the flat fee for the + // short prepaid up front? Same thing with prepaid variable interest. + // /// @dev Mints a pair of long and short positions that directly match each /// other. The amount of long and short positions that are created is /// equal to the base value of the deposit. These positions are sent to @@ -60,6 +63,9 @@ abstract contract HyperdrivePair is _options.extraData ); + // FIXME: We should probably just have a different fee schedule instead + // of re-using the flat fee. + // // The governance fee is twice the governance fee paid on the flat fee // since a long and short are both minted. uint256 governanceFee = 2 * @@ -117,13 +123,67 @@ abstract contract HyperdrivePair is return (maturityTime, bondAmount); } - // FIXME - // function _redeemPair( - // uint256 _amount, - // IHyperdrive.Options calldata _options - // ) internal returns (uint256 maturityTime, uint256 longAmount, uint256 shortAmount) { - // // FIXME - // } + // FIXME: Add Natspec. + function _burn( + uint256 _maturityTime, + uint256 _bondAmount, + IHyperdrive.Options calldata _options + ) + internal + returns (uint256 maturityTime, uint256 longAmount, uint256 shortAmount) + { + // FIXME: This function should take in a long and a short and send the + // underlying capital to the owner. + + // Check that the provided options are valid. + _checkOptions(_options); + + // Ensure that the bond amount is greater than or equal to the minimum + // transaction amount. + if (_bondAmount < _minimumTransactionAmount) { + revert IHyperdrive.MinimumTransactionAmount(); + } + + // If the short hasn't matured, we checkpoint the latest checkpoint. + // Otherwise, we perform a checkpoint at the time the short matured. + // This ensures the short and all of the other positions in the + // checkpoint are closed. + uint256 vaultSharePrice = _pricePerVaultShare(); + if (block.timestamp < _maturityTime) { + _applyCheckpoint( + _latestCheckpoint(), + vaultSharePrice, + LPMath.SHARE_PROCEEDS_MAX_ITERATIONS, + true + ); + } else { + _applyCheckpoint( + _maturityTime, + vaultSharePrice, + LPMath.SHARE_PROCEEDS_MAX_ITERATIONS, + true + ); + } + + // Burn the longs and shorts that are being closed. + _burn( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, _maturityTime), + msg.sender, + _bondAmount + ); + _burn( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, _maturityTime), + msg.sender, + _bondAmount + ); + + // FIXME: We need to do the following things to update the states: + // + // 1. [ ] Update the longs and shorts outstanding. + // 2. [ ] Get the amount of base owed to the longs and shorts. + // 3. [ ] Assess the governance fees. + // 4. [ ] Withdraw the proceeds to the destination. + } /// @dev Applies state changes to create a pair of matched long and short /// positions. This operation leaves the pool's solvency and idle From c96d744d7e2491cf649c8d90d11b6326de27b8bd Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 25 Nov 2024 22:49:45 -0800 Subject: [PATCH 05/45] Updated the `mint` logic and wired it up --- contracts/src/external/Hyperdrive.sol | 11 + contracts/src/external/HyperdriveTarget0.sol | 2 + contracts/src/external/HyperdriveTarget1.sol | 2 + contracts/src/external/HyperdriveTarget2.sol | 2 + contracts/src/external/HyperdriveTarget3.sol | 2 + contracts/src/external/HyperdriveTarget4.sol | 24 ++ contracts/src/interfaces/IHyperdriveCore.sol | 18 ++ contracts/src/internal/HyperdrivePair.sol | 219 ++++++++++++------- test/units/hyperdrive/MintTest.t.sol | 80 +++++++ test/units/hyperdrive/OpenLongTest.t.sol | 1 - 10 files changed, 279 insertions(+), 82 deletions(-) create mode 100644 test/units/hyperdrive/MintTest.t.sol diff --git a/contracts/src/external/Hyperdrive.sol b/contracts/src/external/Hyperdrive.sol index d212f5f47..04ee5f794 100644 --- a/contracts/src/external/Hyperdrive.sol +++ b/contracts/src/external/Hyperdrive.sol @@ -244,6 +244,17 @@ abstract contract Hyperdrive is _delegate(target4); } + /// Pairs /// + + /// @inheritdoc IHyperdriveCore + function mint( + uint256, + uint256, + IHyperdrive.PairOptions calldata + ) external returns (uint256, uint256) { + _delegate(target4); + } + /// Checkpoints /// /// @inheritdoc IHyperdriveCore diff --git a/contracts/src/external/HyperdriveTarget0.sol b/contracts/src/external/HyperdriveTarget0.sol index f97285e47..d68dff609 100644 --- a/contracts/src/external/HyperdriveTarget0.sol +++ b/contracts/src/external/HyperdriveTarget0.sol @@ -10,6 +10,7 @@ import { HyperdriveCheckpoint } from "../internal/HyperdriveCheckpoint.sol"; import { HyperdriveLong } from "../internal/HyperdriveLong.sol"; import { HyperdriveLP } from "../internal/HyperdriveLP.sol"; import { HyperdriveMultiToken } from "../internal/HyperdriveMultiToken.sol"; +import { HyperdrivePair } from "../internal/HyperdrivePair.sol"; import { HyperdriveShort } from "../internal/HyperdriveShort.sol"; import { HyperdriveStorage } from "../internal/HyperdriveStorage.sol"; import { AssetId } from "../libraries/AssetId.sol"; @@ -30,6 +31,7 @@ abstract contract HyperdriveTarget0 is HyperdriveLP, HyperdriveLong, HyperdriveShort, + HyperdrivePair, HyperdriveCheckpoint { using FixedPointMath for uint256; diff --git a/contracts/src/external/HyperdriveTarget1.sol b/contracts/src/external/HyperdriveTarget1.sol index 7f2fbcc14..cdfb853be 100644 --- a/contracts/src/external/HyperdriveTarget1.sol +++ b/contracts/src/external/HyperdriveTarget1.sol @@ -8,6 +8,7 @@ import { HyperdriveCheckpoint } from "../internal/HyperdriveCheckpoint.sol"; import { HyperdriveLong } from "../internal/HyperdriveLong.sol"; import { HyperdriveLP } from "../internal/HyperdriveLP.sol"; import { HyperdriveMultiToken } from "../internal/HyperdriveMultiToken.sol"; +import { HyperdrivePair } from "../internal/HyperdrivePair.sol"; import { HyperdriveShort } from "../internal/HyperdriveShort.sol"; import { HyperdriveStorage } from "../internal/HyperdriveStorage.sol"; @@ -23,6 +24,7 @@ abstract contract HyperdriveTarget1 is HyperdriveLP, HyperdriveLong, HyperdriveShort, + HyperdrivePair, HyperdriveCheckpoint { /// @notice Instantiates target1. diff --git a/contracts/src/external/HyperdriveTarget2.sol b/contracts/src/external/HyperdriveTarget2.sol index dfa004327..5951c5542 100644 --- a/contracts/src/external/HyperdriveTarget2.sol +++ b/contracts/src/external/HyperdriveTarget2.sol @@ -8,6 +8,7 @@ import { HyperdriveCheckpoint } from "../internal/HyperdriveCheckpoint.sol"; import { HyperdriveLong } from "../internal/HyperdriveLong.sol"; import { HyperdriveLP } from "../internal/HyperdriveLP.sol"; import { HyperdriveMultiToken } from "../internal/HyperdriveMultiToken.sol"; +import { HyperdrivePair } from "../internal/HyperdrivePair.sol"; import { HyperdriveShort } from "../internal/HyperdriveShort.sol"; import { HyperdriveStorage } from "../internal/HyperdriveStorage.sol"; @@ -23,6 +24,7 @@ abstract contract HyperdriveTarget2 is HyperdriveLP, HyperdriveLong, HyperdriveShort, + HyperdrivePair, HyperdriveCheckpoint { /// @notice Instantiates target2. diff --git a/contracts/src/external/HyperdriveTarget3.sol b/contracts/src/external/HyperdriveTarget3.sol index aed9e6e3c..1ed46b3ed 100644 --- a/contracts/src/external/HyperdriveTarget3.sol +++ b/contracts/src/external/HyperdriveTarget3.sol @@ -8,6 +8,7 @@ import { HyperdriveCheckpoint } from "../internal/HyperdriveCheckpoint.sol"; import { HyperdriveLong } from "../internal/HyperdriveLong.sol"; import { HyperdriveLP } from "../internal/HyperdriveLP.sol"; import { HyperdriveMultiToken } from "../internal/HyperdriveMultiToken.sol"; +import { HyperdrivePair } from "../internal/HyperdrivePair.sol"; import { HyperdriveShort } from "../internal/HyperdriveShort.sol"; import { HyperdriveStorage } from "../internal/HyperdriveStorage.sol"; @@ -23,6 +24,7 @@ abstract contract HyperdriveTarget3 is HyperdriveLP, HyperdriveLong, HyperdriveShort, + HyperdrivePair, HyperdriveCheckpoint { /// @notice Instantiates target3. diff --git a/contracts/src/external/HyperdriveTarget4.sol b/contracts/src/external/HyperdriveTarget4.sol index b5d8fa6fa..5aea8e236 100644 --- a/contracts/src/external/HyperdriveTarget4.sol +++ b/contracts/src/external/HyperdriveTarget4.sol @@ -8,6 +8,7 @@ import { HyperdriveCheckpoint } from "../internal/HyperdriveCheckpoint.sol"; import { HyperdriveLong } from "../internal/HyperdriveLong.sol"; import { HyperdriveLP } from "../internal/HyperdriveLP.sol"; import { HyperdriveMultiToken } from "../internal/HyperdriveMultiToken.sol"; +import { HyperdrivePair } from "../internal/HyperdrivePair.sol"; import { HyperdriveShort } from "../internal/HyperdriveShort.sol"; import { HyperdriveStorage } from "../internal/HyperdriveStorage.sol"; @@ -23,6 +24,7 @@ abstract contract HyperdriveTarget4 is HyperdriveLP, HyperdriveLong, HyperdriveShort, + HyperdrivePair, HyperdriveCheckpoint { /// @notice Instantiates target4. @@ -86,6 +88,28 @@ abstract contract HyperdriveTarget4 is ); } + /// Pairs /// + + // FIXME: Where does this fit? + // + /// @notice Mints a pair of long and short positions that directly match + /// each other. The amount of long and short positions that are + /// created is equal to the base value of the deposit. These + /// positions are sent to the provided destinations. + /// @param _amount The amount of capital provided to open the long. The + /// units of this quantity are either base or vault shares, depending + /// on the value of `_options.asBase`. + /// @param _options The pair options that configure how the trade is settled. + /// @return maturityTime The maturity time of the new long and short positions. + /// @return bondAmount The bond amount of the new long and short positoins. + function mint( + uint256 _amount, + uint256 _minVaultSharePrice, + IHyperdrive.PairOptions calldata _options + ) external returns (uint256 maturityTime, uint256 bondAmount) { + return _mint(_amount, _minVaultSharePrice, _options); + } + /// Checkpoints /// /// @notice Allows anyone to mint a new checkpoint. diff --git a/contracts/src/interfaces/IHyperdriveCore.sol b/contracts/src/interfaces/IHyperdriveCore.sol index 5301e45eb..d072e074c 100644 --- a/contracts/src/interfaces/IHyperdriveCore.sol +++ b/contracts/src/interfaces/IHyperdriveCore.sol @@ -162,6 +162,24 @@ interface IHyperdriveCore is IMultiTokenCore { IHyperdrive.Options calldata _options ) external returns (uint256 proceeds, uint256 withdrawalSharesRedeemed); + /// Pairs /// + + /// @notice Mints a pair of long and short positions that directly match + /// each other. The amount of long and short positions that are + /// created is equal to the base value of the deposit. These + /// positions are sent to the provided destinations. + /// @param _amount The amount of capital provided to open the long. The + /// units of this quantity are either base or vault shares, depending + /// on the value of `_options.asBase`. + /// @param _options The pair options that configure how the trade is settled. + /// @return maturityTime The maturity time of the new long and short positions. + /// @return bondAmount The bond amount of the new long and short positoins. + function mint( + uint256 _amount, + uint256 _minVaultSharePrice, + IHyperdrive.PairOptions calldata _options + ) external returns (uint256 maturityTime, uint256 bondAmount); + /// Checkpoints /// /// @notice Attempts to mint a checkpoint with the specified checkpoint time. diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index 665bdf863..e564152fa 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -27,9 +27,6 @@ abstract contract HyperdrivePair is using SafeCast for uint256; using SafeCast for int256; - // FIXME: Is there anything weird here about needing the flat fee for the - // short prepaid up front? Same thing with prepaid variable interest. - // /// @dev Mints a pair of long and short positions that directly match each /// other. The amount of long and short positions that are created is /// equal to the base value of the deposit. These positions are sent to @@ -63,19 +60,6 @@ abstract contract HyperdrivePair is _options.extraData ); - // FIXME: We should probably just have a different fee schedule instead - // of re-using the flat fee. - // - // The governance fee is twice the governance fee paid on the flat fee - // since a long and short are both minted. - uint256 governanceFee = 2 * - sharesDeposited.mulDown(_flatFee).mulDown(_governanceLPFee); - _governanceFeesAccrued += governanceFee; - - // The amount of bonds that will be minted is equal to the amount of - // base deposited minus the governance fee. - bondAmount = (sharesDeposited - governanceFee).mulDown(vaultSharePrice); - // Enforce the minimum vault share price. if (vaultSharePrice < _minVaultSharePrice) { revert IHyperdrive.MinimumSharePrice(); @@ -83,16 +67,25 @@ abstract contract HyperdrivePair is // Perform a checkpoint. uint256 latestCheckpoint = _latestCheckpoint(); - _applyCheckpoint( + uint256 openVaultSharePrice = _applyCheckpoint( latestCheckpoint, vaultSharePrice, LPMath.SHARE_PROCEEDS_MAX_ITERATIONS, true ); + // Calculate the bond amount and governance fee from the shares + // deposited. + uint256 governanceFee; + (bondAmount, governanceFee) = _calculateMint( + sharesDeposited, + vaultSharePrice, + openVaultSharePrice + ); + // Apply the state changes caused by creating the pair. maturityTime = latestCheckpoint + _positionDuration; - _applyMint(maturityTime, bondAmount); + _applyMint(maturityTime, bondAmount, governanceFee); // Mint bonds equal in value to the base deposited. uint256 longAssetId = AssetId.encodeAssetId( @@ -107,83 +100,86 @@ abstract contract HyperdrivePair is _mint(shortAssetId, _options.shortDestination, bondAmount); // Emit an Mint event. + uint256 bondAmount_ = bondAmount; // avoid stack-too-deep + uint256 amount = _amount; // avoid stack-too-deep + IHyperdrive.PairOptions calldata options = _options; // avoid stack-too-deep emit Mint( - _options.longDestination, - _options.shortDestination, + options.longDestination, + options.shortDestination, maturityTime, longAssetId, shortAssetId, - _amount, + amount, vaultSharePrice, - _options.asBase, - bondAmount, - _options.extraData + options.asBase, + bondAmount_, + options.extraData ); return (maturityTime, bondAmount); } - // FIXME: Add Natspec. - function _burn( - uint256 _maturityTime, - uint256 _bondAmount, - IHyperdrive.Options calldata _options - ) - internal - returns (uint256 maturityTime, uint256 longAmount, uint256 shortAmount) - { - // FIXME: This function should take in a long and a short and send the - // underlying capital to the owner. + // // FIXME: Add Natspec. + // function _burn( + // uint256 _maturityTime, + // uint256 _bondAmount, + // IHyperdrive.Options calldata _options + // ) + // internal + // returns (uint256 maturityTime, uint256 longAmount, uint256 shortAmount) + // { + // // FIXME: This function should take in a long and a short and send the + // // underlying capital to the owner. - // Check that the provided options are valid. - _checkOptions(_options); + // // Check that the provided options are valid. + // _checkOptions(_options); - // Ensure that the bond amount is greater than or equal to the minimum - // transaction amount. - if (_bondAmount < _minimumTransactionAmount) { - revert IHyperdrive.MinimumTransactionAmount(); - } + // // Ensure that the bond amount is greater than or equal to the minimum + // // transaction amount. + // if (_bondAmount < _minimumTransactionAmount) { + // revert IHyperdrive.MinimumTransactionAmount(); + // } - // If the short hasn't matured, we checkpoint the latest checkpoint. - // Otherwise, we perform a checkpoint at the time the short matured. - // This ensures the short and all of the other positions in the - // checkpoint are closed. - uint256 vaultSharePrice = _pricePerVaultShare(); - if (block.timestamp < _maturityTime) { - _applyCheckpoint( - _latestCheckpoint(), - vaultSharePrice, - LPMath.SHARE_PROCEEDS_MAX_ITERATIONS, - true - ); - } else { - _applyCheckpoint( - _maturityTime, - vaultSharePrice, - LPMath.SHARE_PROCEEDS_MAX_ITERATIONS, - true - ); - } + // // If the short hasn't matured, we checkpoint the latest checkpoint. + // // Otherwise, we perform a checkpoint at the time the short matured. + // // This ensures the short and all of the other positions in the + // // checkpoint are closed. + // uint256 vaultSharePrice = _pricePerVaultShare(); + // if (block.timestamp < _maturityTime) { + // _applyCheckpoint( + // _latestCheckpoint(), + // vaultSharePrice, + // LPMath.SHARE_PROCEEDS_MAX_ITERATIONS, + // true + // ); + // } else { + // _applyCheckpoint( + // _maturityTime, + // vaultSharePrice, + // LPMath.SHARE_PROCEEDS_MAX_ITERATIONS, + // true + // ); + // } - // Burn the longs and shorts that are being closed. - _burn( - AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, _maturityTime), - msg.sender, - _bondAmount - ); - _burn( - AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, _maturityTime), - msg.sender, - _bondAmount - ); + // // Burn the longs and shorts that are being closed. + // _burn( + // AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, _maturityTime), + // msg.sender, + // _bondAmount + // ); + // _burn( + // AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, _maturityTime), + // msg.sender, + // _bondAmount + // ); - // FIXME: We need to do the following things to update the states: - // - // 1. [ ] Update the longs and shorts outstanding. - // 2. [ ] Get the amount of base owed to the longs and shorts. - // 3. [ ] Assess the governance fees. - // 4. [ ] Withdraw the proceeds to the destination. - } + // // FIXME: We need to do the following things to update the states: + // // + // // 1. [ ] Update the longs and shorts outstanding. + // // 2. [ ] Get the amount of base owed to the longs and shorts. + // // 3. [ ] Assess the governance fees. + // // 4. [ ] Withdraw the proceeds to the destination. + // } /// @dev Applies state changes to create a pair of matched long and short /// positions. This operation leaves the pool's solvency and idle @@ -203,7 +199,17 @@ abstract contract HyperdrivePair is /// @param _maturityTime The maturity time of the pair of long and short /// positions /// @param _bondAmount The amount of bonds created. - function _applyMint(uint256 _maturityTime, uint256 _bondAmount) internal { + /// @param _governanceFee The governance fee calculated from the bond amount. + function _applyMint( + uint256 _maturityTime, + uint256 _bondAmount, + uint256 _governanceFee + ) internal { + // Update the amount of governance fees accrued. Since the long and + // short both pay the governance fee, the governance fees accrued + // increases by twice the governance fee. + _governanceFeesAccrued += 2 * _governanceFee; + // Update the average maturity time of longs and short positions and the // amount of long and short positions outstanding. Everything else // remains constant. @@ -230,4 +236,55 @@ abstract contract HyperdrivePair is _marketState.longsOutstanding += _bondAmount.toUint128(); _marketState.shortsOutstanding += _bondAmount.toUint128(); } + + /// @dev Calculates the amount of bonds that can be minted and the governance + /// fee from the amount of vault shares that were deposited. + /// @param _sharesDeposited The amount of vault shares that were deposited. + /// @param _vaultSharePrice The vault share price. + /// @param _openVaultSharePrice The vault share price at the beginning of + /// the checkpoint. + function _calculateMint( + uint256 _sharesDeposited, + uint256 _vaultSharePrice, + uint256 _openVaultSharePrice + ) internal view returns (uint256, uint256) { + // In order for a certain amount of bonds to be minted, there needs to + // be enough base to pay the prepaid interest that has accrued since the + // start of the checkpoint, to pay out the face value of the bond at + // maturity, for the short to pay the flat fee at maturity, and for the + // long and short to both pay the governance fee during the mint. We can + // work back from this understanding to get the amount of bonds from the + // amount of shares deposited. + // + // sharesDeposited * vaultSharePrice = ( + // bondAmount + bondAmount * (c - c0) / c0 + bondAmount * flatFee + + // 2 * bondAmount * flatFee * governanceFee + // ) + // + // This implies that + // + // bondAmount = shareDeposited * vaultSharePrice / ( + // 1 + (c - c0) / c0 + flatFee + 2 * flatFee * governanceFee + // ) + // + // NOTE: We round down to underestimate the bond amount. + uint256 bondAmount = _sharesDeposited.mulDivDown( + _vaultSharePrice, + // NOTE: Round up to overestimate the denominator. This + // underestimates the bond amount. + (ONE + + (_vaultSharePrice - _openVaultSharePrice).divUp( + _openVaultSharePrice + ) + + _flatFee + + 2 * + _flatFee.mulUp(_governanceLPFee)) + ); + // FIXME: What should the rounding be here? + uint256 governanceFee = bondAmount.mulDown(_flatFee).mulDown( + _governanceLPFee + ); + + return (bondAmount, governanceFee); + } } diff --git a/test/units/hyperdrive/MintTest.t.sol b/test/units/hyperdrive/MintTest.t.sol new file mode 100644 index 000000000..4cb5434aa --- /dev/null +++ b/test/units/hyperdrive/MintTest.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.22; + +import { VmSafe } from "forge-std/Vm.sol"; +import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; +import { AssetId } from "../../../contracts/src/libraries/AssetId.sol"; +import { FixedPointMath, ONE } from "../../../contracts/src/libraries/FixedPointMath.sol"; +import { HyperdriveMath } from "../../../contracts/src/libraries/HyperdriveMath.sol"; +import { HyperdriveTest, HyperdriveUtils } from "../../utils/HyperdriveTest.sol"; +import { Lib } from "../../utils/Lib.sol"; + +// FIXME: Add full Natspec. +// +// FIXME: Add all of the failure tests. +// +// FIXME: Add assertions that show that: +// +// - The governance fees were paid. +// - The flat fee of the short was paid. +// - Variable interest was pre-paid. +// - The correct amount of bonds were minted. +// +// To make this test better, we can work backward from the bond amount to the +// full calculation. +// +// This should all contribute to ensuring solvency. +// +// FIXME: Think about what other tests we need once we have this function. +contract MintTest is HyperdriveTest { + using FixedPointMath for uint256; + using HyperdriveUtils for IHyperdrive; + using Lib for *; + + // FIXME: Add a comment. + function setUp() public override { + super.setUp(); + + // Start recording event logs. + vm.recordLogs(); + + // Deploy and initialize a pool with fees. + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + POSITION_DURATION + ); + config.fees.curve = 0.01e18; + config.fees.flat = 0.0005e18; + config.fees.governanceLP = 0.15e18; + deploy(alice, config); + initialize(alice, 0.05e18, 100_000e18); + } + + // FIXME: + function test_mint_zero_amount() external {} + + // FIXME + function test_mint_failure_not_payable() external {} + + // FIXME + function test_mint_failure_destination_zero_address() external {} + + // FIXME + function test_mint_failure_pause() external {} + + // FIXME + function test_mint_failure_minVaultSharePrice() external {} + + // FIXME + function test_mint_success() external {} + + // FIXME + function _verifyMint() internal view { + // FIXME + } + + // FIXME + function verifyOpenLongEvent() internal { + // FIXME + } +} diff --git a/test/units/hyperdrive/OpenLongTest.t.sol b/test/units/hyperdrive/OpenLongTest.t.sol index 6a2e1c033..b50fb2f7d 100644 --- a/test/units/hyperdrive/OpenLongTest.t.sol +++ b/test/units/hyperdrive/OpenLongTest.t.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; -import { stdError } from "forge-std/StdError.sol"; import { VmSafe } from "forge-std/Vm.sol"; import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; import { AssetId } from "../../../contracts/src/libraries/AssetId.sol"; From be28b36ac3965eac2de45b0a4679939c4cbe979f Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 26 Nov 2024 00:38:23 -0800 Subject: [PATCH 06/45] Added unit test cases for the `mint` function --- contracts/src/external/Hyperdrive.sol | 2 +- contracts/src/external/HyperdriveTarget4.sol | 2 +- contracts/src/interfaces/IHyperdriveCore.sol | 2 +- contracts/src/internal/HyperdrivePair.sol | 18 ++ test/units/hyperdrive/MintTest.t.sol | 248 +++++++++++++++++-- test/utils/HyperdriveTest.sol | 87 +++++++ 6 files changed, 340 insertions(+), 19 deletions(-) diff --git a/contracts/src/external/Hyperdrive.sol b/contracts/src/external/Hyperdrive.sol index 04ee5f794..bc23434d2 100644 --- a/contracts/src/external/Hyperdrive.sol +++ b/contracts/src/external/Hyperdrive.sol @@ -251,7 +251,7 @@ abstract contract Hyperdrive is uint256, uint256, IHyperdrive.PairOptions calldata - ) external returns (uint256, uint256) { + ) external payable returns (uint256, uint256) { _delegate(target4); } diff --git a/contracts/src/external/HyperdriveTarget4.sol b/contracts/src/external/HyperdriveTarget4.sol index 5aea8e236..c63ccef1c 100644 --- a/contracts/src/external/HyperdriveTarget4.sol +++ b/contracts/src/external/HyperdriveTarget4.sol @@ -106,7 +106,7 @@ abstract contract HyperdriveTarget4 is uint256 _amount, uint256 _minVaultSharePrice, IHyperdrive.PairOptions calldata _options - ) external returns (uint256 maturityTime, uint256 bondAmount) { + ) external payable returns (uint256 maturityTime, uint256 bondAmount) { return _mint(_amount, _minVaultSharePrice, _options); } diff --git a/contracts/src/interfaces/IHyperdriveCore.sol b/contracts/src/interfaces/IHyperdriveCore.sol index d072e074c..b28e27a89 100644 --- a/contracts/src/interfaces/IHyperdriveCore.sol +++ b/contracts/src/interfaces/IHyperdriveCore.sol @@ -178,7 +178,7 @@ interface IHyperdriveCore is IMultiTokenCore { uint256 _amount, uint256 _minVaultSharePrice, IHyperdrive.PairOptions calldata _options - ) external returns (uint256 maturityTime, uint256 bondAmount); + ) external payable returns (uint256 maturityTime, uint256 bondAmount); /// Checkpoints /// diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index e564152fa..cb8112ea0 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -27,6 +27,9 @@ abstract contract HyperdrivePair is using SafeCast for uint256; using SafeCast for int256; + // FIXME: Do we need a `minOutput` parameter here? It feels kind of paranoid, + // but it's probably a good idea with things like prepaid interest. + // /// @dev Mints a pair of long and short positions that directly match each /// other. The amount of long and short positions that are created is /// equal to the base value of the deposit. These positions are sent to @@ -60,6 +63,21 @@ abstract contract HyperdrivePair is _options.extraData ); + // Enforce the minimum transaction amount. + // + // NOTE: We use the value that is returned from the deposit to check + // against the minimum transaction amount because in the event of + // slippage on the deposit, we want the inputs to the state updates to + // respect the minimum transaction amount requirements. + // + // NOTE: Round down to underestimate the base deposit. This makes the + // minimum transaction amount check more conservative. + if ( + sharesDeposited.mulDown(vaultSharePrice) < _minimumTransactionAmount + ) { + revert IHyperdrive.MinimumTransactionAmount(); + } + // Enforce the minimum vault share price. if (vaultSharePrice < _minVaultSharePrice) { revert IHyperdrive.MinimumSharePrice(); diff --git a/test/units/hyperdrive/MintTest.t.sol b/test/units/hyperdrive/MintTest.t.sol index 4cb5434aa..70f533532 100644 --- a/test/units/hyperdrive/MintTest.t.sol +++ b/test/units/hyperdrive/MintTest.t.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.22; +// FIXME +import { console2 as console } from "forge-std/console2.sol"; + import { VmSafe } from "forge-std/Vm.sol"; import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; import { AssetId } from "../../../contracts/src/libraries/AssetId.sol"; @@ -31,7 +34,7 @@ contract MintTest is HyperdriveTest { using HyperdriveUtils for IHyperdrive; using Lib for *; - // FIXME: Add a comment. + /// @dev Sets up the harness and deploys and initializes a pool with fees. function setUp() public override { super.setUp(); @@ -50,23 +53,231 @@ contract MintTest is HyperdriveTest { initialize(alice, 0.05e18, 100_000e18); } - // FIXME: - function test_mint_zero_amount() external {} + /// @dev Ensures that minting fails when the amount is zero. + function test_mint_failure_zero_amount() external { + vm.stopPrank(); + vm.startPrank(bob); + vm.expectRevert(IHyperdrive.MinimumTransactionAmount.selector); + hyperdrive.mint( + 0, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } - // FIXME - function test_mint_failure_not_payable() external {} + /// @dev Ensures that minting fails when the vault share price is lower than + /// the minimum vault share price. + function test_mint_failure_minVaultSharePrice() external { + vm.stopPrank(); + vm.startPrank(bob); + uint256 basePaid = 10e18; + baseToken.mint(bob, basePaid); + baseToken.approve(address(hyperdrive), basePaid); + uint256 minVaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice * + 2; + vm.expectRevert(IHyperdrive.MinimumSharePrice.selector); + hyperdrive.mint( + basePaid, + minVaultSharePrice, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } - // FIXME - function test_mint_failure_destination_zero_address() external {} + /// @dev Ensures that minting fails when ether is sent to the contract. + function test_mint_failure_not_payable() external { + vm.stopPrank(); + vm.startPrank(bob); + uint256 basePaid = 10e18; + baseToken.mint(bob, basePaid); + baseToken.approve(address(hyperdrive), basePaid); + vm.expectRevert(IHyperdrive.NotPayable.selector); + hyperdrive.mint{ value: 1 }( + basePaid, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } - // FIXME - function test_mint_failure_pause() external {} + /// @dev Ensures that minting fails when the long destination is the zero + /// address. + function test_mint_failure_long_destination_zero_address() external { + vm.stopPrank(); + vm.startPrank(bob); + uint256 basePaid = 10e18; + baseToken.mint(bob, basePaid); + baseToken.approve(address(hyperdrive), basePaid); + vm.expectRevert(IHyperdrive.RestrictedZeroAddress.selector); + hyperdrive.mint( + basePaid, + 0, + IHyperdrive.PairOptions({ + longDestination: address(0), + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } - // FIXME - function test_mint_failure_minVaultSharePrice() external {} + /// @dev Ensures that minting fails when the short destination is the zero + /// address. + function test_mint_failure_short_destination_zero_address() external { + vm.stopPrank(); + vm.startPrank(bob); + uint256 basePaid = 10e18; + baseToken.mint(bob, basePaid); + baseToken.approve(address(hyperdrive), basePaid); + vm.expectRevert(IHyperdrive.RestrictedZeroAddress.selector); + hyperdrive.mint( + basePaid, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: address(0), + asBase: true, + extraData: new bytes(0) + }) + ); + } - // FIXME - function test_mint_success() external {} + /// @dev Ensures that minting fails when the pool is paused. + function test_mint_failure_pause() external { + pause(true); + vm.stopPrank(); + vm.startPrank(bob); + uint256 basePaid = 10e18; + baseToken.mint(bob, basePaid); + baseToken.approve(address(hyperdrive), basePaid); + vm.expectRevert(IHyperdrive.PoolIsPaused.selector); + hyperdrive.mint( + basePaid, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + pause(false); + } + + // FIXME: This case would be better if there was actually pre-paid interest. + // + /// @dev Ensures that minting performs correctly when it succeeds. + function test_mint_success() external { + // Mint some base tokens to Alice and approve Hyperdrive. + vm.stopPrank(); + vm.startPrank(alice); + uint256 baseAmount = 100_000e18; + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + + // Get some data before minting. + uint256 maturityTime = hyperdrive.latestCheckpoint() + + hyperdrive.getPoolConfig().positionDuration; + uint256 idleBefore = hyperdrive.idle(); + uint256 kBefore = hyperdrive.k(); + uint256 spotPriceBefore = hyperdrive.calculateSpotPrice(); + uint256 aliceBaseBalanceBefore = baseToken.balanceOf(address(alice)); + uint256 aliceLongBalanceBefore = hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + alice + ); + uint256 hyperdriveBaseBalanceBefore = baseToken.balanceOf( + address(hyperdrive) + ); + uint256 governanceFeesAccruedBefore = hyperdrive + .getUncollectedGovernanceFees(); + + // Ensure that Alice can successfully mint. + vm.stopPrank(); + vm.startPrank(alice); + (uint256 maturityTime_, uint256 bondAmount) = hyperdrive.mint( + baseAmount, + 0, + IHyperdrive.PairOptions({ + longDestination: alice, + shortDestination: alice, + asBase: true, + extraData: "" + }) + ); + assertEq(maturityTime_, maturityTime); + + // Verify that the balances increased and decreased by the right amounts. + assertEq( + baseToken.balanceOf(alice), + aliceBaseBalanceBefore - baseAmount + ); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + alice + ), + aliceLongBalanceBefore + bondAmount + ); + assertEq( + baseToken.balanceOf(address(hyperdrive)), + hyperdriveBaseBalanceBefore + baseAmount + ); + + // Verify that idle, spot price, and pool depth are all + // unchanged. + assertEq(hyperdrive.idle(), idleBefore); + assertEq(hyperdrive.calculateSpotPrice(), spotPriceBefore); + assertEq(hyperdrive.k(), kBefore); + + // Ensure that the governance fees accrued increased by the right amount. + assertEq( + hyperdrive.getUncollectedGovernanceFees(), + governanceFeesAccruedBefore + + 2 * + bondAmount + .mulDown(hyperdrive.getPoolConfig().fees.flat) + .mulDown(hyperdrive.getPoolConfig().fees.governanceLP) + ); + + // FIXME: This is good, but we need test cases that verify that this + // interoperates with the rest of the Hyperdrive system. + // + // Ensure that the base amount is the bond amount plus the prepaid + // variable interest plus the governance fees plus the prepaid flat fee. + uint256 openVaultSharePrice = hyperdrive + .getCheckpoint(hyperdrive.latestCheckpoint()) + .vaultSharePrice; + uint256 requiredBaseAmount = bondAmount + + bondAmount.mulDivDown( + hyperdrive.getPoolInfo().vaultSharePrice - openVaultSharePrice, + openVaultSharePrice + ) + + bondAmount.mulDown(hyperdrive.getPoolConfig().fees.flat) + + 2 * + bondAmount.mulDown(hyperdrive.getPoolConfig().fees.flat).mulDown( + hyperdrive.getPoolConfig().fees.governanceLP + ); + assertGt(baseAmount, requiredBaseAmount); + assertApproxEqAbs(baseAmount, requiredBaseAmount, 2); + + // FIXME: Verify the event. + } + + // FIXME: We can add more cases for minting success. // FIXME function _verifyMint() internal view { @@ -74,7 +285,12 @@ contract MintTest is HyperdriveTest { } // FIXME - function verifyOpenLongEvent() internal { - // FIXME - } + // function verifyMintEvent() internal { + // VmSafe.Log[] memory logs = vm.getRecordedLogs().filterLogs( + // Mint.selector + // ); + // assertEq(logs.length, 1); + // VmSafe.Log memory log = logs[0]; + // assertEq(address(uint160(uint256(log.topics[1]))), destination); + // } } diff --git a/test/utils/HyperdriveTest.sol b/test/utils/HyperdriveTest.sol index c616032c9..879f6bbe2 100644 --- a/test/utils/HyperdriveTest.sol +++ b/test/utils/HyperdriveTest.sol @@ -859,6 +859,93 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { ); } + function mint( + address trader, + uint256 baseAmount, + DepositOverrides memory overrides + ) internal returns (uint256 maturityTime, uint256 bondAmount) { + vm.stopPrank(); + vm.startPrank(trader); + + // Mint the bonds. + hyperdrive.getPoolConfig(); + if ( + address(hyperdrive.getPoolConfig().baseToken) == address(ETH) && + overrides.asBase + ) { + return + hyperdrive.mint{ value: overrides.depositAmount }( + baseAmount, + overrides.minSharePrice, // min vault share price + IHyperdrive.PairOptions({ + // FIXME: It might be good to test with both. + longDestination: overrides.destination, + shortDestination: overrides.destination, + asBase: overrides.asBase, + extraData: overrides.extraData + }) + ); + } else { + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + return + hyperdrive.mint( + baseAmount, + overrides.minSharePrice, // min vault share price + IHyperdrive.PairOptions({ + // FIXME: It might be good to test with both. + longDestination: overrides.destination, + shortDestination: overrides.destination, + asBase: overrides.asBase, + extraData: overrides.extraData + }) + ); + } + } + + function mint( + address trader, + uint256 baseAmount + ) internal returns (uint256 maturityTime, uint256 bondAmount) { + return + mint( + trader, + baseAmount, + DepositOverrides({ + asBase: true, + destination: trader, + depositAmount: baseAmount, + minSharePrice: 0, // min vault share price of 0 + // FIXME: Should this be unused? + minSlippage: 0, // unused + maxSlippage: type(uint256).max, // unused + extraData: new bytes(0) // unused + }) + ); + } + + function mint( + address trader, + uint256 amount, + bool asBase + ) internal returns (uint256 maturityTime, uint256 bondAmount) { + return + mint( + trader, + amount, + DepositOverrides({ + asBase: asBase, + destination: trader, + depositAmount: amount, + minSharePrice: 0, // min vault share price of 0 + // FIXME: Should this be unused? + minSlippage: 0, // unused + maxSlippage: type(uint256).max, // unused + extraData: new bytes(0) // unused + }) + ); + } + /// Utils /// function advanceTime(uint256 time, int256 variableRate) internal virtual { From 94a124a24006b9da6e6241116ab02a7a5b1c4c69 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 27 Nov 2024 12:33:06 -0800 Subject: [PATCH 07/45] Adds comprehensive unit tests for the mint function --- test/units/hyperdrive/MintTest.t.sol | 301 ++++++++++++++++++++------- 1 file changed, 228 insertions(+), 73 deletions(-) diff --git a/test/units/hyperdrive/MintTest.t.sol b/test/units/hyperdrive/MintTest.t.sol index 70f533532..b390d496e 100644 --- a/test/units/hyperdrive/MintTest.t.sol +++ b/test/units/hyperdrive/MintTest.t.sol @@ -1,9 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.22; -// FIXME -import { console2 as console } from "forge-std/console2.sol"; - import { VmSafe } from "forge-std/Vm.sol"; import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; import { AssetId } from "../../../contracts/src/libraries/AssetId.sol"; @@ -12,23 +9,7 @@ import { HyperdriveMath } from "../../../contracts/src/libraries/HyperdriveMath. import { HyperdriveTest, HyperdriveUtils } from "../../utils/HyperdriveTest.sol"; import { Lib } from "../../utils/Lib.sol"; -// FIXME: Add full Natspec. -// -// FIXME: Add all of the failure tests. -// -// FIXME: Add assertions that show that: -// -// - The governance fees were paid. -// - The flat fee of the short was paid. -// - Variable interest was pre-paid. -// - The correct amount of bonds were minted. -// -// To make this test better, we can work backward from the bond amount to the -// full calculation. -// -// This should all contribute to ensuring solvency. -// -// FIXME: Think about what other tests we need once we have this function. +/// @dev A test suite for the mint function. contract MintTest is HyperdriveTest { using FixedPointMath for uint256; using HyperdriveUtils for IHyperdrive; @@ -189,73 +170,214 @@ contract MintTest is HyperdriveTest { baseToken.approve(address(hyperdrive), baseAmount); // Get some data before minting. - uint256 maturityTime = hyperdrive.latestCheckpoint() + - hyperdrive.getPoolConfig().positionDuration; - uint256 idleBefore = hyperdrive.idle(); - uint256 kBefore = hyperdrive.k(); - uint256 spotPriceBefore = hyperdrive.calculateSpotPrice(); - uint256 aliceBaseBalanceBefore = baseToken.balanceOf(address(alice)); - uint256 aliceLongBalanceBefore = hyperdrive.balanceOf( - AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), - alice + MintTestCase memory testCase = _mintTestCase( + alice, // funder + bob, // long + celine, // short + baseAmount, // amount + true, // asBase + "" // extraData ); - uint256 hyperdriveBaseBalanceBefore = baseToken.balanceOf( - address(hyperdrive) + + // Verify the mint transaction. + _verifyMint(testCase); + } + + /// @dev Ensures that minting performs correctly when it succeeds. + function test_mint_success_prepaid_interest() external { + // Mint some base tokens to Alice and approve Hyperdrive. + vm.stopPrank(); + vm.startPrank(alice); + uint256 baseAmount = 100_000e18; + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + + // Mint a checkpoint and accrue interest. This sets us up to have + // prepaid interest to account for. + hyperdrive.checkpoint(hyperdrive.latestCheckpoint(), 0); + advanceTime(CHECKPOINT_DURATION.mulDown(0.5e18), 2.5e18); + + // Get some data before minting. + MintTestCase memory testCase = _mintTestCase( + alice, // funder + bob, // long + celine, // short + baseAmount, // amount + true, // asBase + "" // extraData ); - uint256 governanceFeesAccruedBefore = hyperdrive - .getUncollectedGovernanceFees(); + // Verify the mint transaction. + _verifyMint(testCase); + } + + struct MintTestCase { + // Trading metadata. + address funder; + address long; + address short; + uint256 maturityTime; + uint256 amount; + bool asBase; + bytes extraData; + // The balances before the mint. + uint256 funderBaseBalanceBefore; + uint256 hyperdriveBaseBalanceBefore; + uint256 longBalanceBefore; + uint256 shortBalanceBefore; + // The state variables before the mint. + uint256 longsOutstandingBefore; + uint256 shortsOutstandingBefore; + uint256 governanceFeesAccruedBefore; + // Idle, pool depth, and spot price before the mint. + uint256 idleBefore; + uint256 kBefore; + uint256 spotPriceBefore; + uint256 lpSharePriceBefore; + } + + /// @dev Creates the test case for the mint transaction. + /// @param _funder The funder of the mint. + /// @param _long The long destination. + /// @param _short The short destination. + /// @param _amount The amount of base or vault shares to deposit. + /// @param _asBase A flag indicating whether or not the deposit is in base + /// or vault shares. + /// @param _extraData The extra data for the transaction. + function _mintTestCase( + address _funder, + address _long, + address _short, + uint256 _amount, + bool _asBase, + bytes memory _extraData + ) internal view returns (MintTestCase memory) { + uint256 maturityTime = hyperdrive.latestCheckpoint() + + hyperdrive.getPoolConfig().positionDuration; + return + MintTestCase({ + // Trading metadata. + funder: _funder, + long: _long, + short: _short, + maturityTime: maturityTime, + amount: _amount, + asBase: _asBase, + extraData: _extraData, + // The balances before the mint. + funderBaseBalanceBefore: baseToken.balanceOf(_funder), + hyperdriveBaseBalanceBefore: baseToken.balanceOf( + address(hyperdrive) + ), + longBalanceBefore: hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + maturityTime + ), + _long + ), + shortBalanceBefore: hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + maturityTime + ), + _short + ), + // The state variables before the mint. + longsOutstandingBefore: hyperdrive + .getPoolInfo() + .longsOutstanding, + shortsOutstandingBefore: hyperdrive + .getPoolInfo() + .shortsOutstanding, + governanceFeesAccruedBefore: hyperdrive + .getUncollectedGovernanceFees(), + // Idle, pool depth, and spot price before the mint. + idleBefore: hyperdrive.idle(), + kBefore: hyperdrive.k(), + spotPriceBefore: hyperdrive.calculateSpotPrice(), + lpSharePriceBefore: hyperdrive.getPoolInfo().lpSharePrice + }); + } + + /// @dev Process a mint transaction and verify that the state was updated + /// correctly. + /// @param _testCase The test case for the mint test. + function _verifyMint(MintTestCase memory _testCase) internal { // Ensure that Alice can successfully mint. vm.stopPrank(); vm.startPrank(alice); - (uint256 maturityTime_, uint256 bondAmount) = hyperdrive.mint( - baseAmount, + (uint256 maturityTime, uint256 bondAmount) = hyperdrive.mint( + _testCase.amount, 0, IHyperdrive.PairOptions({ - longDestination: alice, - shortDestination: alice, - asBase: true, - extraData: "" + longDestination: _testCase.long, + shortDestination: _testCase.short, + asBase: _testCase.asBase, + extraData: _testCase.extraData }) ); - assertEq(maturityTime_, maturityTime); + assertEq(maturityTime, _testCase.maturityTime); // Verify that the balances increased and decreased by the right amounts. assertEq( - baseToken.balanceOf(alice), - aliceBaseBalanceBefore - baseAmount + baseToken.balanceOf(_testCase.funder), + _testCase.funderBaseBalanceBefore - _testCase.amount ); assertEq( hyperdrive.balanceOf( - AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), - alice + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + _testCase.maturityTime + ), + _testCase.long ), - aliceLongBalanceBefore + bondAmount + _testCase.longBalanceBefore + bondAmount + ); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + _testCase.maturityTime + ), + _testCase.short + ), + _testCase.shortBalanceBefore + bondAmount ); assertEq( baseToken.balanceOf(address(hyperdrive)), - hyperdriveBaseBalanceBefore + baseAmount + _testCase.hyperdriveBaseBalanceBefore + _testCase.amount ); - // Verify that idle, spot price, and pool depth are all + // Verify that idle, spot price, LP share price, and pool depth are all // unchanged. - assertEq(hyperdrive.idle(), idleBefore); - assertEq(hyperdrive.calculateSpotPrice(), spotPriceBefore); - assertEq(hyperdrive.k(), kBefore); + assertEq(hyperdrive.idle(), _testCase.idleBefore); + assertEq(hyperdrive.calculateSpotPrice(), _testCase.spotPriceBefore); + assertEq(hyperdrive.k(), _testCase.kBefore); + assertEq( + hyperdrive.getPoolInfo().lpSharePrice, + _testCase.lpSharePriceBefore + ); - // Ensure that the governance fees accrued increased by the right amount. + // Ensure that the longs outstanding, shorts outstanding, and governance + // fees accrued increased by the right amount. + assertEq( + hyperdrive.getPoolInfo().longsOutstanding, + _testCase.longsOutstandingBefore + bondAmount + ); + assertEq( + hyperdrive.getPoolInfo().shortsOutstanding, + _testCase.shortsOutstandingBefore + bondAmount + ); assertEq( hyperdrive.getUncollectedGovernanceFees(), - governanceFeesAccruedBefore + + _testCase.governanceFeesAccruedBefore + 2 * bondAmount .mulDown(hyperdrive.getPoolConfig().fees.flat) .mulDown(hyperdrive.getPoolConfig().fees.governanceLP) ); - // FIXME: This is good, but we need test cases that verify that this - // interoperates with the rest of the Hyperdrive system. - // // Ensure that the base amount is the bond amount plus the prepaid // variable interest plus the governance fees plus the prepaid flat fee. uint256 openVaultSharePrice = hyperdrive @@ -271,26 +393,59 @@ contract MintTest is HyperdriveTest { bondAmount.mulDown(hyperdrive.getPoolConfig().fees.flat).mulDown( hyperdrive.getPoolConfig().fees.governanceLP ); - assertGt(baseAmount, requiredBaseAmount); - assertApproxEqAbs(baseAmount, requiredBaseAmount, 2); + assertGt(_testCase.amount, requiredBaseAmount); + assertApproxEqAbs(_testCase.amount, requiredBaseAmount, 10); - // FIXME: Verify the event. + // Verify the `Mint` event. + _verifyMintEvent(_testCase, bondAmount); } - // FIXME: We can add more cases for minting success. - - // FIXME - function _verifyMint() internal view { - // FIXME + /// @dev Verify the mint event. + /// @param _testCase The test case containing all of the metadata and data + /// relating to the mint transaction. + /// @param _bondAmount The amount of bonds that were minted. + function _verifyMintEvent( + MintTestCase memory _testCase, + uint256 _bondAmount + ) internal { + VmSafe.Log[] memory logs = vm.getRecordedLogs().filterLogs( + Mint.selector + ); + assertEq(logs.length, 1); + VmSafe.Log memory log = logs[0]; + assertEq(address(uint160(uint256(log.topics[1]))), _testCase.long); + assertEq(address(uint160(uint256(log.topics[2]))), _testCase.short); + assertEq(uint256(log.topics[3]), _testCase.maturityTime); + ( + uint256 longAssetId, + uint256 shortAssetId, + uint256 amount, + uint256 vaultSharePrice, + bool asBase, + uint256 bondAmount, + bytes memory extraData + ) = abi.decode( + log.data, + (uint256, uint256, uint256, uint256, bool, uint256, bytes) + ); + assertEq( + longAssetId, + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + _testCase.maturityTime + ) + ); + assertEq( + shortAssetId, + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + _testCase.maturityTime + ) + ); + assertEq(amount, _testCase.amount); + assertEq(vaultSharePrice, hyperdrive.getPoolInfo().vaultSharePrice); + assertEq(asBase, _testCase.asBase); + assertEq(bondAmount, _bondAmount); + assertEq(extraData, _testCase.extraData); } - - // FIXME - // function verifyMintEvent() internal { - // VmSafe.Log[] memory logs = vm.getRecordedLogs().filterLogs( - // Mint.selector - // ); - // assertEq(logs.length, 1); - // VmSafe.Log memory log = logs[0]; - // assertEq(address(uint160(uint256(log.topics[1]))), destination); - // } } From 75bd986a3daf5459152cec04a0eb39a0303ca9a7 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 4 Dec 2024 16:10:18 -0600 Subject: [PATCH 08/45] Adds a `minOutput` parameter to `mint` --- contracts/src/external/Hyperdrive.sol | 1 + contracts/src/external/HyperdriveTarget4.sol | 8 ++- contracts/src/interfaces/IHyperdriveCore.sol | 6 +++ contracts/src/internal/HyperdrivePair.sol | 26 +++++++--- test/integrations/hyperdrive/Mint.t.sol | 54 ++++++++++++++++++++ test/units/hyperdrive/MintTest.t.sol | 36 +++++++++++-- test/utils/HyperdriveTest.sol | 10 ++-- 7 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 test/integrations/hyperdrive/Mint.t.sol diff --git a/contracts/src/external/Hyperdrive.sol b/contracts/src/external/Hyperdrive.sol index bc23434d2..387f214ef 100644 --- a/contracts/src/external/Hyperdrive.sol +++ b/contracts/src/external/Hyperdrive.sol @@ -248,6 +248,7 @@ abstract contract Hyperdrive is /// @inheritdoc IHyperdriveCore function mint( + uint256, uint256, uint256, IHyperdrive.PairOptions calldata diff --git a/contracts/src/external/HyperdriveTarget4.sol b/contracts/src/external/HyperdriveTarget4.sol index c63ccef1c..e08075544 100644 --- a/contracts/src/external/HyperdriveTarget4.sol +++ b/contracts/src/external/HyperdriveTarget4.sol @@ -99,15 +99,21 @@ abstract contract HyperdriveTarget4 is /// @param _amount The amount of capital provided to open the long. The /// units of this quantity are either base or vault shares, depending /// on the value of `_options.asBase`. + /// @param _minOutput The minimum number of bonds to receive. + /// @param _minVaultSharePrice The minimum vault share price at which to + /// mint the bonds. This allows traders to protect themselves from + /// opening a long in a checkpoint where negative interest has + /// accrued. /// @param _options The pair options that configure how the trade is settled. /// @return maturityTime The maturity time of the new long and short positions. /// @return bondAmount The bond amount of the new long and short positoins. function mint( uint256 _amount, + uint256 _minOutput, uint256 _minVaultSharePrice, IHyperdrive.PairOptions calldata _options ) external payable returns (uint256 maturityTime, uint256 bondAmount) { - return _mint(_amount, _minVaultSharePrice, _options); + return _mint(_amount, _minOutput, _minVaultSharePrice, _options); } /// Checkpoints /// diff --git a/contracts/src/interfaces/IHyperdriveCore.sol b/contracts/src/interfaces/IHyperdriveCore.sol index b28e27a89..659f851eb 100644 --- a/contracts/src/interfaces/IHyperdriveCore.sol +++ b/contracts/src/interfaces/IHyperdriveCore.sol @@ -171,11 +171,17 @@ interface IHyperdriveCore is IMultiTokenCore { /// @param _amount The amount of capital provided to open the long. The /// units of this quantity are either base or vault shares, depending /// on the value of `_options.asBase`. + /// @param _minOutput The minimum number of bonds to receive. + /// @param _minVaultSharePrice The minimum vault share price at which to + /// mint the bonds. This allows traders to protect themselves from + /// opening a long in a checkpoint where negative interest has + /// accrued. /// @param _options The pair options that configure how the trade is settled. /// @return maturityTime The maturity time of the new long and short positions. /// @return bondAmount The bond amount of the new long and short positoins. function mint( uint256 _amount, + uint256 _minOutput, uint256 _minVaultSharePrice, IHyperdrive.PairOptions calldata _options ) external payable returns (uint256 maturityTime, uint256 bondAmount); diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index cb8112ea0..dd01f79ed 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -5,7 +5,6 @@ import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; import { IHyperdriveEvents } from "../interfaces/IHyperdriveEvents.sol"; import { AssetId } from "../libraries/AssetId.sol"; import { FixedPointMath, ONE } from "../libraries/FixedPointMath.sol"; -import { HyperdriveMath } from "../libraries/HyperdriveMath.sol"; import { LPMath } from "../libraries/LPMath.sol"; import { SafeCast } from "../libraries/SafeCast.sol"; import { HyperdriveBase } from "./HyperdriveLP.sol"; @@ -27,9 +26,6 @@ abstract contract HyperdrivePair is using SafeCast for uint256; using SafeCast for int256; - // FIXME: Do we need a `minOutput` parameter here? It feels kind of paranoid, - // but it's probably a good idea with things like prepaid interest. - // /// @dev Mints a pair of long and short positions that directly match each /// other. The amount of long and short positions that are created is /// equal to the base value of the deposit. These positions are sent to @@ -37,11 +33,17 @@ abstract contract HyperdrivePair is /// @param _amount The amount of capital provided to open the long. The /// units of this quantity are either base or vault shares, depending /// on the value of `_options.asBase`. + /// @param _minOutput The minimum number of bonds to receive. + /// @param _minVaultSharePrice The minimum vault share price at which to + /// mint the bonds. This allows traders to protect themselves from + /// opening a long in a checkpoint where negative interest has + /// accrued. /// @param _options The pair options that configure how the trade is settled. /// @return maturityTime The maturity time of the new long and short positions. /// @return bondAmount The bond amount of the new long and short positoins. function _mint( uint256 _amount, + uint256 _minOutput, uint256 _minVaultSharePrice, IHyperdrive.PairOptions calldata _options ) @@ -101,6 +103,11 @@ abstract contract HyperdrivePair is openVaultSharePrice ); + // Enforce the minimum user outputs. + if (bondAmount < _minOutput) { + revert IHyperdrive.OutputLimit(); + } + // Apply the state changes caused by creating the pair. maturityTime = latestCheckpoint + _positionDuration; _applyMint(maturityTime, bondAmount, governanceFee); @@ -298,8 +305,15 @@ abstract contract HyperdrivePair is 2 * _flatFee.mulUp(_governanceLPFee)) ); - // FIXME: What should the rounding be here? - uint256 governanceFee = bondAmount.mulDown(_flatFee).mulDown( + + // The governance fee that will be paid on both the long and the short + // sides of the trade is given by: + // + // governanceFee = bondAmount * flatFee * governanceLPFee + // + // NOTE: Round the flat fee calculation up and the governance fee + // calculation down to match the rounding used in the other flows. + uint256 governanceFee = bondAmount.mulUp(_flatFee).mulDown( _governanceLPFee ); diff --git a/test/integrations/hyperdrive/Mint.t.sol b/test/integrations/hyperdrive/Mint.t.sol new file mode 100644 index 000000000..d3cc3ddd7 --- /dev/null +++ b/test/integrations/hyperdrive/Mint.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.22; + +import { VmSafe } from "forge-std/Vm.sol"; +import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; +import { AssetId } from "../../../contracts/src/libraries/AssetId.sol"; +import { FixedPointMath, ONE } from "../../../contracts/src/libraries/FixedPointMath.sol"; +import { HyperdriveMath } from "../../../contracts/src/libraries/HyperdriveMath.sol"; +import { HyperdriveTest, HyperdriveUtils } from "../../utils/HyperdriveTest.sol"; +import { Lib } from "../../utils/Lib.sol"; + +/// @dev An integration test suite for the mint function. +contract MintTest is HyperdriveTest { + using FixedPointMath for uint256; + using HyperdriveUtils for IHyperdrive; + using Lib for *; + + /// @dev Sets up the harness and deploys and initializes a pool with fees. + function setUp() public override { + // Run the higher level setup function. + super.setUp(); + + // Deploy and initialize a pool with fees. + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + POSITION_DURATION + ); + config.fees.curve = 0.01e18; + config.fees.flat = 0.0005e18; + config.fees.governanceLP = 0.15e18; + deploy(alice, config); + initialize(alice, 0.05e18, 100_000e18); + } + + // FIXME: We need integration tests that ensure that mint interoperates with + // the rest of the AMM. Here are a few tests that we should be particularly + // mindful of: + // + // - Mint and then close long. + // - Mint and then close short. + // - Mint and then add liquidity. + // - Mint and then remove liquidity. + // + // Some things to think about are: + // + // - Mint's affect on solvency + // - Mint's affect on pricing + // - Whether or not anything is unaccounted for after closing the position. + + // FIXME + function test_mint_and_close_long() external { + // + } +} diff --git a/test/units/hyperdrive/MintTest.t.sol b/test/units/hyperdrive/MintTest.t.sol index b390d496e..d0e91617b 100644 --- a/test/units/hyperdrive/MintTest.t.sol +++ b/test/units/hyperdrive/MintTest.t.sol @@ -4,8 +4,7 @@ pragma solidity 0.8.22; import { VmSafe } from "forge-std/Vm.sol"; import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; import { AssetId } from "../../../contracts/src/libraries/AssetId.sol"; -import { FixedPointMath, ONE } from "../../../contracts/src/libraries/FixedPointMath.sol"; -import { HyperdriveMath } from "../../../contracts/src/libraries/HyperdriveMath.sol"; +import { FixedPointMath } from "../../../contracts/src/libraries/FixedPointMath.sol"; import { HyperdriveTest, HyperdriveUtils } from "../../utils/HyperdriveTest.sol"; import { Lib } from "../../utils/Lib.sol"; @@ -17,6 +16,7 @@ contract MintTest is HyperdriveTest { /// @dev Sets up the harness and deploys and initializes a pool with fees. function setUp() public override { + // Run the higher level setup function. super.setUp(); // Start recording event logs. @@ -40,6 +40,7 @@ contract MintTest is HyperdriveTest { vm.startPrank(bob); vm.expectRevert(IHyperdrive.MinimumTransactionAmount.selector); hyperdrive.mint( + 0, 0, 0, IHyperdrive.PairOptions({ @@ -64,6 +65,7 @@ contract MintTest is HyperdriveTest { vm.expectRevert(IHyperdrive.MinimumSharePrice.selector); hyperdrive.mint( basePaid, + 0, minVaultSharePrice, IHyperdrive.PairOptions({ longDestination: bob, @@ -74,6 +76,29 @@ contract MintTest is HyperdriveTest { ); } + /// @dev Ensures that minting fails when the bond proceeds is lower than + /// the minimum output. + function test_mint_failure_minOutput() external { + vm.stopPrank(); + vm.startPrank(bob); + uint256 basePaid = 10e18; + baseToken.mint(bob, basePaid); + baseToken.approve(address(hyperdrive), basePaid); + uint256 minOutput = 15e18; + vm.expectRevert(IHyperdrive.OutputLimit.selector); + hyperdrive.mint( + basePaid, + minOutput, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } + /// @dev Ensures that minting fails when ether is sent to the contract. function test_mint_failure_not_payable() external { vm.stopPrank(); @@ -85,6 +110,7 @@ contract MintTest is HyperdriveTest { hyperdrive.mint{ value: 1 }( basePaid, 0, + 0, IHyperdrive.PairOptions({ longDestination: bob, shortDestination: bob, @@ -106,6 +132,7 @@ contract MintTest is HyperdriveTest { hyperdrive.mint( basePaid, 0, + 0, IHyperdrive.PairOptions({ longDestination: address(0), shortDestination: bob, @@ -127,6 +154,7 @@ contract MintTest is HyperdriveTest { hyperdrive.mint( basePaid, 0, + 0, IHyperdrive.PairOptions({ longDestination: bob, shortDestination: address(0), @@ -148,6 +176,7 @@ contract MintTest is HyperdriveTest { hyperdrive.mint( basePaid, 0, + 0, IHyperdrive.PairOptions({ longDestination: bob, shortDestination: bob, @@ -158,8 +187,6 @@ contract MintTest is HyperdriveTest { pause(false); } - // FIXME: This case would be better if there was actually pre-paid interest. - // /// @dev Ensures that minting performs correctly when it succeeds. function test_mint_success() external { // Mint some base tokens to Alice and approve Hyperdrive. @@ -310,6 +337,7 @@ contract MintTest is HyperdriveTest { (uint256 maturityTime, uint256 bondAmount) = hyperdrive.mint( _testCase.amount, 0, + 0, IHyperdrive.PairOptions({ longDestination: _testCase.long, shortDestination: _testCase.short, diff --git a/test/utils/HyperdriveTest.sol b/test/utils/HyperdriveTest.sol index 879f6bbe2..f0f59773c 100644 --- a/test/utils/HyperdriveTest.sol +++ b/test/utils/HyperdriveTest.sol @@ -876,9 +876,9 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { return hyperdrive.mint{ value: overrides.depositAmount }( baseAmount, + overrides.minSlippage, // min output overrides.minSharePrice, // min vault share price IHyperdrive.PairOptions({ - // FIXME: It might be good to test with both. longDestination: overrides.destination, shortDestination: overrides.destination, asBase: overrides.asBase, @@ -891,9 +891,9 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { return hyperdrive.mint( baseAmount, + overrides.minSlippage, // min output overrides.minSharePrice, // min vault share price IHyperdrive.PairOptions({ - // FIXME: It might be good to test with both. longDestination: overrides.destination, shortDestination: overrides.destination, asBase: overrides.asBase, @@ -916,8 +916,7 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { destination: trader, depositAmount: baseAmount, minSharePrice: 0, // min vault share price of 0 - // FIXME: Should this be unused? - minSlippage: 0, // unused + minSlippage: 0, // min output of 0 maxSlippage: type(uint256).max, // unused extraData: new bytes(0) // unused }) @@ -938,8 +937,7 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { destination: trader, depositAmount: amount, minSharePrice: 0, // min vault share price of 0 - // FIXME: Should this be unused? - minSlippage: 0, // unused + minSlippage: 0, // min output of 0 maxSlippage: type(uint256).max, // unused extraData: new bytes(0) // unused }) From 7817f25ccbb327bff9170c45c78d678d02fad556 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 6 Dec 2024 16:54:23 -0600 Subject: [PATCH 09/45] Wrote a comprehensive integration test suite for the `mint` function --- test/integrations/hyperdrive/Mint.t.sol | 54 ---- test/integrations/hyperdrive/MintTest.t.sol | 271 ++++++++++++++++++++ 2 files changed, 271 insertions(+), 54 deletions(-) delete mode 100644 test/integrations/hyperdrive/Mint.t.sol create mode 100644 test/integrations/hyperdrive/MintTest.t.sol diff --git a/test/integrations/hyperdrive/Mint.t.sol b/test/integrations/hyperdrive/Mint.t.sol deleted file mode 100644 index d3cc3ddd7..000000000 --- a/test/integrations/hyperdrive/Mint.t.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.22; - -import { VmSafe } from "forge-std/Vm.sol"; -import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; -import { AssetId } from "../../../contracts/src/libraries/AssetId.sol"; -import { FixedPointMath, ONE } from "../../../contracts/src/libraries/FixedPointMath.sol"; -import { HyperdriveMath } from "../../../contracts/src/libraries/HyperdriveMath.sol"; -import { HyperdriveTest, HyperdriveUtils } from "../../utils/HyperdriveTest.sol"; -import { Lib } from "../../utils/Lib.sol"; - -/// @dev An integration test suite for the mint function. -contract MintTest is HyperdriveTest { - using FixedPointMath for uint256; - using HyperdriveUtils for IHyperdrive; - using Lib for *; - - /// @dev Sets up the harness and deploys and initializes a pool with fees. - function setUp() public override { - // Run the higher level setup function. - super.setUp(); - - // Deploy and initialize a pool with fees. - IHyperdrive.PoolConfig memory config = testConfig( - 0.05e18, - POSITION_DURATION - ); - config.fees.curve = 0.01e18; - config.fees.flat = 0.0005e18; - config.fees.governanceLP = 0.15e18; - deploy(alice, config); - initialize(alice, 0.05e18, 100_000e18); - } - - // FIXME: We need integration tests that ensure that mint interoperates with - // the rest of the AMM. Here are a few tests that we should be particularly - // mindful of: - // - // - Mint and then close long. - // - Mint and then close short. - // - Mint and then add liquidity. - // - Mint and then remove liquidity. - // - // Some things to think about are: - // - // - Mint's affect on solvency - // - Mint's affect on pricing - // - Whether or not anything is unaccounted for after closing the position. - - // FIXME - function test_mint_and_close_long() external { - // - } -} diff --git a/test/integrations/hyperdrive/MintTest.t.sol b/test/integrations/hyperdrive/MintTest.t.sol new file mode 100644 index 000000000..f23cac906 --- /dev/null +++ b/test/integrations/hyperdrive/MintTest.t.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.22; + +import { VmSafe } from "forge-std/Vm.sol"; +import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; +import { AssetId } from "../../../contracts/src/libraries/AssetId.sol"; +import { FixedPointMath, ONE } from "../../../contracts/src/libraries/FixedPointMath.sol"; +import { HyperdriveMath } from "../../../contracts/src/libraries/HyperdriveMath.sol"; +import { HyperdriveTest, HyperdriveUtils } from "../../utils/HyperdriveTest.sol"; +import { Lib } from "../../utils/Lib.sol"; + +/// @dev An integration test suite for the mint function. +contract MintTest is HyperdriveTest { + using FixedPointMath for uint256; + using HyperdriveUtils for IHyperdrive; + using Lib for *; + + /// @dev Sets up the harness and deploys and initializes a pool with fees. + function setUp() public override { + // Run the higher level setup function. + super.setUp(); + + // Deploy and initialize a pool with the flat fee and governance LP fee + // turned on. The curve fee is turned off to simplify the assertions. + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + POSITION_DURATION + ); + config.fees.flat = 0.0005e18; + config.fees.governanceLP = 0.15e18; + deploy(alice, config); + initialize(alice, 0.05e18, 100_000e18); + } + + /// @dev Ensures that minting and closing positions instantaneously works as + /// expected. In particular, we want to ensure that: + /// + /// - Idle increases by the flat fees and is less than or equal to the + /// base balance of Hyperdrive. + /// - The spot price remains the same. + /// - The pool depth remains the same. + /// - The trader gets the right amount of money from closing their + /// positions. + function test_mint_and_close_instantaneously(uint256 _baseAmount) external { + // Get some data before minting and closing the positions. + uint256 spotPriceBefore = hyperdrive.calculateSpotPrice(); + uint256 kBefore = hyperdrive.k(); + uint256 idleBefore = hyperdrive.idle(); + + // Alice mints some bonds. + _baseAmount = _baseAmount.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + 20_000e18 + ); + (uint256 maturityTime, uint256 bondAmount) = mint(alice, _baseAmount); + + // Alice closes the long and short instantaneously. + uint256 longProceeds = closeLong(alice, maturityTime, bondAmount); + uint256 shortProceeds = closeShort(alice, maturityTime, bondAmount); + + // Ensure that Alice's total proceeds are less than the amount of base + // paid. Furthermore, we assert that the proceeds are approximately + // equal to the base amount minus the governance fees. + assertLt(longProceeds + shortProceeds, _baseAmount); + assertApproxEqAbs( + longProceeds + shortProceeds, + _baseAmount - + 2 * + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + hyperdrive.getPoolConfig().fees.governanceLP + ), + 1e6 + ); + + // Ensure that the spot price didn't change. + assertApproxEqAbs(hyperdrive.calculateSpotPrice(), spotPriceBefore, 1); + + // Ensure that the pool depth didn't change. + assertApproxEqAbs(hyperdrive.k(), kBefore, 1e6); + + // Ensure that the idle stayed roughly constant during this trade. + // Furthermore, we can assert that the pool's idle is less than or equal + // to the base balance of the Hyperdrive pool. This ensures that the + // pool thinks it is solvent and that it actually is solvent. + assertApproxEqAbs(hyperdrive.idle(), idleBefore, 1e6); + assertLe(hyperdrive.idle(), baseToken.balanceOf(address(hyperdrive))); + } + + /// @dev Ensures that minting and closing positions before maturity works as + /// expected. In particular, we want to ensure that: + /// + /// - Idle increases by the flat fees and is less than or equal to the + /// base balance of Hyperdrive. + /// - The spot price remains the same. + /// - The pool depth remains the same. + /// - The trader gets the right amount of money from closing their + /// positions. + /// @param _baseAmount The amount of base to use when minting the positions. + /// @param _timeDelta The amount of time that passes. This is greater than + /// no time and less than the position duration. + /// @param _variableRate The variable rate when time passes. + function test_mint_and_close_before_maturity( + uint256 _baseAmount, + uint256 _timeDelta, + int256 _variableRate + ) external { + // Get some data before minting and closing the positions. + uint256 vaultSharePriceBefore = hyperdrive + .getPoolInfo() + .vaultSharePrice; + uint256 spotPriceBefore = hyperdrive.calculateSpotPrice(); + uint256 idleBefore = hyperdrive.idle(); + + // Alice mints some bonds. + _baseAmount = _baseAmount.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + 20_000e18 + ); + (uint256 maturityTime, uint256 bondAmount) = mint(alice, _baseAmount); + + // Part of the term passes and interest accrues. + _variableRate = _variableRate.normalizeToRange(0, 2.5e18); + _timeDelta = _timeDelta.normalizeToRange( + 1, + hyperdrive.getPoolConfig().positionDuration - 1 + ); + advanceTime(_timeDelta, _variableRate); + + // Alice closes the long and short before maturity. + uint256 longProceeds = closeLong(alice, maturityTime, bondAmount); + uint256 shortProceeds = closeShort(alice, maturityTime, bondAmount); + + // Ensure that Alice's total proceeds are less than the base amount + // scaled by the amount of interest that accrued. + uint256 baseAmount = _baseAmount; // avoid stack-too-deep + assertLt( + longProceeds + shortProceeds, + baseAmount.mulDivDown( + hyperdrive.getPoolInfo().vaultSharePrice, + vaultSharePriceBefore + ) + ); + + // Ensure that Alice received the correct amount of proceeds from + // closing her position. She should receive the par value of the bond + // plus any interest that accrued over the term. Additionally, she + // receives the flat fee that she repaid. Then she pays the flat fee + // twice. The flat fee that she pays is scaled for the amount of time + // that passed since minting the bonds. + assertApproxEqAbs( + longProceeds + shortProceeds, + bondAmount.mulDivDown( + hyperdrive.getPoolInfo().vaultSharePrice, + vaultSharePriceBefore + ) + + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat) - + 2 * + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + ONE - hyperdrive.calculateTimeRemaining(maturityTime) + ), + 1e6 + ); + + // Ensure that the spot price didn't change. + assertApproxEqAbs(hyperdrive.calculateSpotPrice(), spotPriceBefore, 1); + + // Ensure that the idle only increased by the flat fee from each trade. + // Furthermore, we can assert that the pool's idle is less than or equal + // to the base balance of the Hyperdrive pool. This ensures that the + // pool thinks it is solvent and that it actually is solvent. + assertApproxEqAbs( + hyperdrive.idle(), + idleBefore.mulDivDown( + hyperdrive.getPoolInfo().vaultSharePrice, + vaultSharePriceBefore + ) + + 2 * + bondAmount + .mulUp(hyperdrive.getPoolConfig().fees.flat) + .mulDown( + ONE - hyperdrive.calculateTimeRemaining(maturityTime) + ) + .mulDown( + ONE - hyperdrive.getPoolConfig().fees.governanceLP + ), + 1e6 + ); + assertLe(hyperdrive.idle(), baseToken.balanceOf(address(hyperdrive))); + } + + /// @dev Ensures that minting and closing positions at maturity works as + /// expected. In particular, we want to ensure that: + /// + /// - Idle increases by the flat fees and is less than or equal to the + /// base balance of Hyperdrive. + /// - The spot price remains the same. + /// @param _baseAmount The amount of base to use when minting the positions. + /// @param _variableRate The variable rate when time passes. + function test_mint_and_close_at_maturity( + uint256 _baseAmount, + int256 _variableRate + ) external { + // Get some data before minting and closing the positions. + uint256 vaultSharePriceBefore = hyperdrive + .getPoolInfo() + .vaultSharePrice; + uint256 spotPriceBefore = hyperdrive.calculateSpotPrice(); + uint256 idleBefore = hyperdrive.idle(); + + // Alice mints some bonds. + _baseAmount = _baseAmount.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + 20_000e18 + ); + (uint256 maturityTime, uint256 bondAmount) = mint(alice, _baseAmount); + + // The term passes and interest accrues. + _variableRate = _variableRate.normalizeToRange(0, 2.5e18); + advanceTime(hyperdrive.getPoolConfig().positionDuration, _variableRate); + + // Alice closes the long and short at maturity. + uint256 longProceeds = closeLong(alice, maturityTime, bondAmount); + uint256 shortProceeds = closeShort(alice, maturityTime, bondAmount); + + // Ensure that Alice's total proceeds are less than the base amount + // scaled by the amount of interest that accrued. + uint256 baseAmount = _baseAmount; // avoid stack-too-deep + assertLt( + longProceeds + shortProceeds, + baseAmount.mulDivDown( + hyperdrive.getPoolInfo().vaultSharePrice, + vaultSharePriceBefore + ) + ); + + // Ensure that Alice received the correct amount of proceeds from + // closing her position. She should receive the par value of the bond + // plus any interest that accrued over the term. Additionally, she + // receives the flat fee that she repaid. Then she pays the flat fee + // twice. + assertApproxEqAbs( + longProceeds + shortProceeds, + bondAmount.mulDivDown( + hyperdrive.getPoolInfo().vaultSharePrice, + vaultSharePriceBefore + ) - bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat), + 1e6 + ); + + // Ensure that the spot price didn't change. + assertEq(hyperdrive.calculateSpotPrice(), spotPriceBefore); + + // Ensure that the idle only increased by the flat fee from each trade. + // Furthermore, we can assert that the pool's idle is less than or equal + // to the base balance of the Hyperdrive pool. This ensures that the + // pool thinks it is solvent and that it actually is solvent. + assertApproxEqAbs( + hyperdrive.idle(), + idleBefore.mulDivDown( + hyperdrive.getPoolInfo().vaultSharePrice, + vaultSharePriceBefore + ) + + 2 * + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + ONE - hyperdrive.getPoolConfig().fees.governanceLP + ), + 1e6 + ); + assertLe(hyperdrive.idle(), baseToken.balanceOf(address(hyperdrive))); + } +} From 1daa62fae4ae612f82de1e47ab59532ee65e76a9 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 2 Jan 2025 11:08:51 -0600 Subject: [PATCH 10/45] Started implementing `burn` --- .../src/interfaces/IHyperdriveEvents.sol | 13 + contracts/src/internal/HyperdrivePair.sol | 302 ++++++++++++++---- 2 files changed, 250 insertions(+), 65 deletions(-) diff --git a/contracts/src/interfaces/IHyperdriveEvents.sol b/contracts/src/interfaces/IHyperdriveEvents.sol index 26f5d5e34..e1256b6d0 100644 --- a/contracts/src/interfaces/IHyperdriveEvents.sol +++ b/contracts/src/interfaces/IHyperdriveEvents.sol @@ -116,6 +116,19 @@ interface IHyperdriveEvents is IMultiTokenEvents { bytes extraData ); + /// @notice Emitted when a pair of long and short positions are burned. + event Burn( + address indexed trader, + uint256 indexed maturityTime, + uint256 longAssetId, + uint256 shortAssetId, + uint256 amount, + uint256 vaultSharePrice, + bool asBase, + uint256 bondAmount, + bytes extraData + ); + /// @notice Emitted when a checkpoint is created. event CreateCheckpoint( uint256 indexed checkpointTime, diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index dd01f79ed..ea64ddcb6 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -10,9 +10,11 @@ import { SafeCast } from "../libraries/SafeCast.sol"; import { HyperdriveBase } from "./HyperdriveLP.sol"; import { HyperdriveMultiToken } from "./HyperdriveMultiToken.sol"; +// FIXME: Update the comments to use standard notation. +// /// @author DELV -/// @title HyperdriveLong -/// @notice Implements the long accounting for Hyperdrive. +/// @title HyperdrivePair +/// @notice Implements the pair accounting for Hyperdrive. /// @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. @@ -26,6 +28,9 @@ abstract contract HyperdrivePair is using SafeCast for uint256; using SafeCast for int256; + // FIXME: Comb through this. Fix any comments that are out of date. Make + // sure we aren't forgetting anything we do in the other flows. + // /// @dev Mints a pair of long and short positions that directly match each /// other. The amount of long and short positions that are created is /// equal to the base value of the deposit. These positions are sent to @@ -108,7 +113,8 @@ abstract contract HyperdrivePair is revert IHyperdrive.OutputLimit(); } - // Apply the state changes caused by creating the pair. + // Apply the state changes caused by minting the offsetting longs and + // shorts. maturityTime = latestCheckpoint + _positionDuration; _applyMint(maturityTime, bondAmount, governanceFee); @@ -124,7 +130,7 @@ abstract contract HyperdrivePair is _mint(longAssetId, _options.longDestination, bondAmount); _mint(shortAssetId, _options.shortDestination, bondAmount); - // Emit an Mint event. + // Emit a Mint event. uint256 bondAmount_ = bondAmount; // avoid stack-too-deep uint256 amount = _amount; // avoid stack-too-deep IHyperdrive.PairOptions calldata options = _options; // avoid stack-too-deep @@ -144,68 +150,105 @@ abstract contract HyperdrivePair is return (maturityTime, bondAmount); } - // // FIXME: Add Natspec. - // function _burn( - // uint256 _maturityTime, - // uint256 _bondAmount, - // IHyperdrive.Options calldata _options - // ) - // internal - // returns (uint256 maturityTime, uint256 longAmount, uint256 shortAmount) - // { - // // FIXME: This function should take in a long and a short and send the - // // underlying capital to the owner. - - // // Check that the provided options are valid. - // _checkOptions(_options); - - // // Ensure that the bond amount is greater than or equal to the minimum - // // transaction amount. - // if (_bondAmount < _minimumTransactionAmount) { - // revert IHyperdrive.MinimumTransactionAmount(); - // } - - // // If the short hasn't matured, we checkpoint the latest checkpoint. - // // Otherwise, we perform a checkpoint at the time the short matured. - // // This ensures the short and all of the other positions in the - // // checkpoint are closed. - // uint256 vaultSharePrice = _pricePerVaultShare(); - // if (block.timestamp < _maturityTime) { - // _applyCheckpoint( - // _latestCheckpoint(), - // vaultSharePrice, - // LPMath.SHARE_PROCEEDS_MAX_ITERATIONS, - // true - // ); - // } else { - // _applyCheckpoint( - // _maturityTime, - // vaultSharePrice, - // LPMath.SHARE_PROCEEDS_MAX_ITERATIONS, - // true - // ); - // } - - // // Burn the longs and shorts that are being closed. - // _burn( - // AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, _maturityTime), - // msg.sender, - // _bondAmount - // ); - // _burn( - // AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, _maturityTime), - // msg.sender, - // _bondAmount - // ); - - // // FIXME: We need to do the following things to update the states: - // // - // // 1. [ ] Update the longs and shorts outstanding. - // // 2. [ ] Get the amount of base owed to the longs and shorts. - // // 3. [ ] Assess the governance fees. - // // 4. [ ] Withdraw the proceeds to the destination. - // } + // FIXME: Comb through this. Fix any comments that are out of date. Make + // sure we aren't forgetting anything we do in the other flows. + // + // FIXME: Handle negative interest. Handle zombie interest. Any reason to + // call distribute excess idle? + // + /// @dev Burns a pair of long and short positions that directly match each + /// other. The capital underlying these positions is released to the + /// trader burning the positions. + /// @param _maturityTime The maturity time of the long and short positions. + /// @param _bondAmount The amount of longs and shorts to close. + /// @param _minOutput The minimum amount of proceeds to receive. + /// @param _options The options that configure how the trade is settled. + /// @return The proceeds the user receives. The units of this quantity are + /// either base or vault shares, depending on the value of + /// `_options.asBase`. + function _burn( + uint256 _maturityTime, + uint256 _bondAmount, + uint256 _minOutput, + IHyperdrive.Options calldata _options + ) internal nonReentrant returns (uint256 proceeds) { + // Check that the provided options are valid. + _checkOptions(_options); + + // Ensure that the bond amount is greater than or equal to the minimum + // transaction amount. + if (_bondAmount < _minimumTransactionAmount) { + revert IHyperdrive.MinimumTransactionAmount(); + } + + // FIXME: Update the comments. + // + // If the short hasn't matured, we checkpoint the latest checkpoint. + // Otherwise, we perform a checkpoint at the time the short matured. + // This ensures the short and all of the other positions in the + // checkpoint are closed. + uint256 vaultSharePrice = _pricePerVaultShare(); + if (block.timestamp < _maturityTime) { + _applyCheckpoint( + _latestCheckpoint(), + vaultSharePrice, + LPMath.SHARE_PROCEEDS_MAX_ITERATIONS, + true + ); + } else { + _applyCheckpoint( + _maturityTime, + vaultSharePrice, + LPMath.SHARE_PROCEEDS_MAX_ITERATIONS, + true + ); + } + + // Burn the longs and shorts that are being closed. + uint256 longAssetId = AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + _maturityTime + ); + _burn(longAssetId, msg.sender, _bondAmount); + uint256 shortAssetId = AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + _maturityTime + ); + _burn(shortAssetId, msg.sender, _bondAmount); + + // Calculate the proceeds of burning the bonds with the specified + // maturity. + (uint256 shareProceeds, uint256 governanceFee) = _calculateBurn( + _maturityTime, + _bondAmount, + vaultSharePrice + ); + + // Apply the state changes caused by burning the offsetting longs and + // shorts. + _applyBurn(maturityTime, bondAmount, governanceFee); + + // Withdraw the profit to the trader. + proceeds = _withdraw(shareProceeds, vaultSharePrice, _options); + + // Emit a Burn event. + emit Burn( + msg.sender, + _maturityTime, + longAssetId, + shortAssetId, + proceeds, + vaultSharePrice, + _options.asBase, + _bondAmount, + _options.extraData + ); + return proceeds; + } + + // FIXME: How does this work with negative interest? + // /// @dev Applies state changes to create a pair of matched long and short /// positions. This operation leaves the pool's solvency and idle /// capital unchanged because the positions fully net out. Specifically: @@ -262,6 +305,69 @@ abstract contract HyperdrivePair is _marketState.shortsOutstanding += _bondAmount.toUint128(); } + // FIXME: How does this handle negative interest. + // + // FIXME: Natspec + // + /// @dev Applies state changes to burn a pair of matched long and short + /// positions and release the underlying funds. This operation leaves + /// the pool's solvency and idle capital unchanged because the + /// positions fully net out. Specifically: + /// + /// - Share reserves, share adjustments, and bond reserves remain + /// constant since the released capital backs the positions directly. + /// - Solvency remains constant because the net effect of burning + /// matching long and short positions is neutral. + /// - Idle capital is unaffected since no excess funds are added or + /// removed during this process. + /// + /// Therefore: + /// + /// - Solvency checks are unnecessary. + /// - Idle capital does not need to be redistributed to LPs. + /// @param _maturityTime The maturity time of the pair of long and short + /// positions + /// @param _bondAmount The amount of bonds burned. + /// @param _governanceFee The governance fee calculated from the bond amount. + function _applyBurn( + uint256 _maturityTime, + uint256 _bondAmount, + uint256 _governanceFee + ) internal { + // Update the amount of governance fees accrued. Since the long and + // short both pay the governance fee, the governance fees accrued + // increases by twice the governance fee. + _governanceFeesAccrued += 2 * _governanceFee; + + // Update the average maturity time of longs and short positions and the + // amount of long and short positions outstanding. Everything else + // remains constant. + _marketState.longAverageMaturityTime = uint256( + _marketState.longAverageMaturityTime + ) + .updateWeightedAverage( + _marketState.longsOutstanding, + _maturityTime * ONE, // scale up to fixed point scale + _bondAmount, + false + ) + .toUint128(); + _marketState.shortAverageMaturityTime = uint256( + _marketState.shortAverageMaturityTime + ) + .updateWeightedAverage( + _marketState.shortsOutstanding, + _maturityTime * ONE, // scale up to fixed point scale + _bondAmount, + false + ) + .toUint128(); + _marketState.longsOutstanding -= _bondAmount.toUint128(); + _marketState.shortsOutstanding -= _bondAmount.toUint128(); + } + + // FIXME: How does this work with negative interest? + // /// @dev Calculates the amount of bonds that can be minted and the governance /// fee from the amount of vault shares that were deposited. /// @param _sharesDeposited The amount of vault shares that were deposited. @@ -295,6 +401,8 @@ abstract contract HyperdrivePair is // NOTE: We round down to underestimate the bond amount. uint256 bondAmount = _sharesDeposited.mulDivDown( _vaultSharePrice, + // FIXME: What should we do if `_vaultSharePrice < _openVaultSharePrice`? + // // NOTE: Round up to overestimate the denominator. This // underestimates the bond amount. (ONE + @@ -306,6 +414,8 @@ abstract contract HyperdrivePair is _flatFee.mulUp(_governanceLPFee)) ); + // FIXME: We normally do these calculations in shares. This seems wrong. + // // The governance fee that will be paid on both the long and the short // sides of the trade is given by: // @@ -319,4 +429,66 @@ abstract contract HyperdrivePair is return (bondAmount, governanceFee); } + + // FIXME: How does this work with negative interest? + // + // FIXME: Natspec. + // + // FIXME: Comment on the rounding. + function _calculateBurn( + uint256 _maturityTime, + uint256 _bondAmount, + uint256 _vaultSharePrice + ) internal view returns (uint256, uint256) { + // The short's pre-paid flat fee in shares that will be refunded. This + // is given by: + // + // flatFee = bondAmount * flatFee / vaultSharePrice + // + // NOTE: Round the flat fee calculation up to match the rounding used in + // the other flows. + uint256 flatFee = _bondAmount.mulDivUp(_flatFee, _vaultSharePrice); + + // The governance fee in shares that will be paid on both the long and + // the short sides of the trade is given by: + // + // governanceFee = bondAmount * flatFee * governanceLPFee / vaultSharePrice + // + // NOTE: Round the flat fee calculation up and the governance fee + // calculation down to match the rounding used in the other flows. + uint256 governanceFee = _bondAmount.mulUp(_flatFee).mulDivDown( + _governanceLPFee, + _vaultSharesPrice + ); + + // FIXME: Double check this accounting. + // + // The total amount of value underlying the longs and shorts in shares + // is the face value of the bonds plus the amount of interest that + // accrued on the face value. We then add the flat fee to this quantity + // since this was pre-paid by the short and needs to be refunded. + // Finally, we subtract twice the governance fee. All of this is given + // by: + // + // totalValue = (c1 / (c * c0)) * bondAmount + flatFee - 2 * governanceFee + uint256 openVaultSharePrice = _checkpoints[ + _maturityTime - _positionDuration + ].vaultSharePrice; + uint256 closeVaultSharePrice = block.timestamp < _maturityTime + ? _vaultSharePrice + : _checkpoints[_maturityTime].vaultSharePrice; + uint256 flatFee = _bondAmount.mulDivUp(_flatFee, _vaultSharePrice); + uint256 shareProceeds = _bondAmount.mulDivDown( + closeVaultSharePrice, + _vaultSharePrice.mulDown(openVaultSharePrice) + ) + + flatFee - + 2 * + governanceFee; + + // FIXME: Calculate negative interest. Do we want to do this with or + // without fees? + + return (shareProceeds, governanceFee); + } } From f3c16279b40c521d2e76df2c450cea6161cc53b1 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 2 Jan 2025 17:02:48 -0600 Subject: [PATCH 11/45] Made some targeted fixes to `mint` --- contracts/src/internal/HyperdrivePair.sol | 56 ++++++++++----------- test/integrations/hyperdrive/MintTest.t.sol | 4 +- test/units/hyperdrive/MintTest.t.sol | 7 ++- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index ea64ddcb6..45df3d1c0 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -10,7 +10,15 @@ import { SafeCast } from "../libraries/SafeCast.sol"; import { HyperdriveBase } from "./HyperdriveLP.sol"; import { HyperdriveMultiToken } from "./HyperdriveMultiToken.sol"; -// FIXME: Update the comments to use standard notation. +// FIXME: There are several remaining todos: +// +// - [ ] Handle negative interest. +// - [ ] Handle zombie interest. +// - [ ] Consider distributing excess idle. This will be necessary depending on +// how negative interest is handled. +// - [ ] Test negative interest. +// - [ ] Test zombie interest. +// - [ ] Update the comments to use standard notation. // /// @author DELV /// @title HyperdrivePair @@ -163,9 +171,9 @@ abstract contract HyperdrivePair is /// @param _bondAmount The amount of longs and shorts to close. /// @param _minOutput The minimum amount of proceeds to receive. /// @param _options The options that configure how the trade is settled. - /// @return The proceeds the user receives. The units of this quantity are - /// either base or vault shares, depending on the value of - /// `_options.asBase`. + /// @return proceeds The proceeds the user receives. The units of this + /// quantity are either base or vault shares, depending on the value + /// of `_options.asBase`. function _burn( uint256 _maturityTime, uint256 _bondAmount, @@ -181,11 +189,9 @@ abstract contract HyperdrivePair is revert IHyperdrive.MinimumTransactionAmount(); } - // FIXME: Update the comments. - // - // If the short hasn't matured, we checkpoint the latest checkpoint. - // Otherwise, we perform a checkpoint at the time the short matured. - // This ensures the short and all of the other positions in the + // If the pair hasn't matured, we checkpoint the latest checkpoint. + // Otherwise, we perform a checkpoint at the time the pair matured. + // This ensures the pair and all of the other positions in the // checkpoint are closed. uint256 vaultSharePrice = _pricePerVaultShare(); if (block.timestamp < _maturityTime) { @@ -226,7 +232,7 @@ abstract contract HyperdrivePair is // Apply the state changes caused by burning the offsetting longs and // shorts. - _applyBurn(maturityTime, bondAmount, governanceFee); + _applyBurn(_maturityTime, _bondAmount, governanceFee); // Withdraw the profit to the trader. proceeds = _withdraw(shareProceeds, vaultSharePrice, _options); @@ -247,8 +253,6 @@ abstract contract HyperdrivePair is return proceeds; } - // FIXME: How does this work with negative interest? - // /// @dev Applies state changes to create a pair of matched long and short /// positions. This operation leaves the pool's solvency and idle /// capital unchanged because the positions fully net out. Specifically: @@ -366,8 +370,6 @@ abstract contract HyperdrivePair is _marketState.shortsOutstanding -= _bondAmount.toUint128(); } - // FIXME: How does this work with negative interest? - // /// @dev Calculates the amount of bonds that can be minted and the governance /// fee from the amount of vault shares that were deposited. /// @param _sharesDeposited The amount of vault shares that were deposited. @@ -388,7 +390,7 @@ abstract contract HyperdrivePair is // amount of shares deposited. // // sharesDeposited * vaultSharePrice = ( - // bondAmount + bondAmount * (c - c0) / c0 + bondAmount * flatFee + + // bondAmount + bondAmount * (max(c, c0) - c0) / c0 + bondAmount * flatFee + // 2 * bondAmount * flatFee * governanceFee // ) // @@ -401,30 +403,29 @@ abstract contract HyperdrivePair is // NOTE: We round down to underestimate the bond amount. uint256 bondAmount = _sharesDeposited.mulDivDown( _vaultSharePrice, - // FIXME: What should we do if `_vaultSharePrice < _openVaultSharePrice`? - // // NOTE: Round up to overestimate the denominator. This // underestimates the bond amount. (ONE + - (_vaultSharePrice - _openVaultSharePrice).divUp( - _openVaultSharePrice - ) + + // NOTE: If negative interest has accrued and the open vault + // share price is greater than the vault share price, we clamp + // the vault share price to the open vault share price. + (_vaultSharePrice.max(_openVaultSharePrice) - + _openVaultSharePrice).divUp(_openVaultSharePrice) + _flatFee + 2 * _flatFee.mulUp(_governanceLPFee)) ); - // FIXME: We normally do these calculations in shares. This seems wrong. - // // The governance fee that will be paid on both the long and the short - // sides of the trade is given by: + // sides of the trade in shares is given by: // - // governanceFee = bondAmount * flatFee * governanceLPFee + // governanceFee = bondAmount * flatFee * governanceLPFee / vaultSharePrice // // NOTE: Round the flat fee calculation up and the governance fee // calculation down to match the rounding used in the other flows. - uint256 governanceFee = bondAmount.mulUp(_flatFee).mulDown( - _governanceLPFee + uint256 governanceFee = bondAmount.mulUp(_flatFee).mulDivDown( + _governanceLPFee, + _vaultSharePrice ); return (bondAmount, governanceFee); @@ -458,7 +459,7 @@ abstract contract HyperdrivePair is // calculation down to match the rounding used in the other flows. uint256 governanceFee = _bondAmount.mulUp(_flatFee).mulDivDown( _governanceLPFee, - _vaultSharesPrice + _vaultSharePrice ); // FIXME: Double check this accounting. @@ -477,7 +478,6 @@ abstract contract HyperdrivePair is uint256 closeVaultSharePrice = block.timestamp < _maturityTime ? _vaultSharePrice : _checkpoints[_maturityTime].vaultSharePrice; - uint256 flatFee = _bondAmount.mulDivUp(_flatFee, _vaultSharePrice); uint256 shareProceeds = _bondAmount.mulDivDown( closeVaultSharePrice, _vaultSharePrice.mulDown(openVaultSharePrice) diff --git a/test/integrations/hyperdrive/MintTest.t.sol b/test/integrations/hyperdrive/MintTest.t.sol index f23cac906..7a8308c91 100644 --- a/test/integrations/hyperdrive/MintTest.t.sol +++ b/test/integrations/hyperdrive/MintTest.t.sol @@ -158,7 +158,7 @@ contract MintTest is HyperdriveTest { bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( ONE - hyperdrive.calculateTimeRemaining(maturityTime) ), - 1e6 + 1e7 ); // Ensure that the spot price didn't change. @@ -183,7 +183,7 @@ contract MintTest is HyperdriveTest { .mulDown( ONE - hyperdrive.getPoolConfig().fees.governanceLP ), - 1e6 + 1e7 ); assertLe(hyperdrive.idle(), baseToken.balanceOf(address(hyperdrive))); } diff --git a/test/units/hyperdrive/MintTest.t.sol b/test/units/hyperdrive/MintTest.t.sol index d0e91617b..166a5d6d0 100644 --- a/test/units/hyperdrive/MintTest.t.sol +++ b/test/units/hyperdrive/MintTest.t.sol @@ -402,8 +402,11 @@ contract MintTest is HyperdriveTest { _testCase.governanceFeesAccruedBefore + 2 * bondAmount - .mulDown(hyperdrive.getPoolConfig().fees.flat) - .mulDown(hyperdrive.getPoolConfig().fees.governanceLP) + .mulUp(hyperdrive.getPoolConfig().fees.flat) + .mulDivDown( + hyperdrive.getPoolConfig().fees.governanceLP, + hyperdrive.getPoolInfo().vaultSharePrice + ) ); // Ensure that the base amount is the bond amount plus the prepaid From 886ad438b4a04959fdca9f265089fac2f4cc0cdb Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 3 Jan 2025 10:14:03 -0600 Subject: [PATCH 12/45] Added zombie interest to the `burn` flow and cleaned up `HyperdrivePair` --- contracts/src/internal/HyperdrivePair.sol | 159 ++++++++++++---------- 1 file changed, 90 insertions(+), 69 deletions(-) diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index 45df3d1c0..a14c39f3e 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -7,38 +7,20 @@ import { AssetId } from "../libraries/AssetId.sol"; import { FixedPointMath, ONE } from "../libraries/FixedPointMath.sol"; import { LPMath } from "../libraries/LPMath.sol"; import { SafeCast } from "../libraries/SafeCast.sol"; -import { HyperdriveBase } from "./HyperdriveLP.sol"; -import { HyperdriveMultiToken } from "./HyperdriveMultiToken.sol"; - -// FIXME: There are several remaining todos: -// -// - [ ] Handle negative interest. -// - [ ] Handle zombie interest. -// - [ ] Consider distributing excess idle. This will be necessary depending on -// how negative interest is handled. -// - [ ] Test negative interest. -// - [ ] Test zombie interest. -// - [ ] Update the comments to use standard notation. -// +import { HyperdriveLP } from "./HyperdriveLP.sol"; + /// @author DELV /// @title HyperdrivePair /// @notice Implements the pair accounting for Hyperdrive. /// @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. -abstract contract HyperdrivePair is - IHyperdriveEvents, - HyperdriveBase, - HyperdriveMultiToken -{ +abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { using FixedPointMath for uint256; using FixedPointMath for int256; using SafeCast for uint256; using SafeCast for int256; - // FIXME: Comb through this. Fix any comments that are out of date. Make - // sure we aren't forgetting anything we do in the other flows. - // /// @dev Mints a pair of long and short positions that directly match each /// other. The amount of long and short positions that are created is /// equal to the base value of the deposit. These positions are sent to @@ -158,12 +140,6 @@ abstract contract HyperdrivePair is return (maturityTime, bondAmount); } - // FIXME: Comb through this. Fix any comments that are out of date. Make - // sure we aren't forgetting anything we do in the other flows. - // - // FIXME: Handle negative interest. Handle zombie interest. Any reason to - // call distribute excess idle? - // /// @dev Burns a pair of long and short positions that directly match each /// other. The capital underlying these positions is released to the /// trader burning the positions. @@ -230,13 +206,43 @@ abstract contract HyperdrivePair is vaultSharePrice ); - // Apply the state changes caused by burning the offsetting longs and - // shorts. - _applyBurn(_maturityTime, _bondAmount, governanceFee); + // If the positions haven't matured, apply the accounting updates that + // result from closing the pair to the reserves. + if (block.timestamp < _maturityTime) { + // Apply the state changes caused by burning the offsetting longs and + // shorts. + // + // NOTE: Since the spot price doesn't change, we don't update the + // weighted average spot price in this transaction. Similarly, since + // idle doesn't change, we don't distribute excess idle here. It's + // possible that a small amount of interest has accrued, but this + // doesn't warrant the extra gas expenditure. + _applyBurn(_maturityTime, _bondAmount, governanceFee); + } else { + // Apply the zombie close to the state and adjust the share proceeds + // to account for negative interest that might have accrued to the + // zombie share reserves. + shareProceeds = _applyZombieClose(shareProceeds, vaultSharePrice); + + // Distribute the excess idle to the withdrawal pool. If the + // distribute excess idle calculation fails, we proceed with the + // calculation since traders should be able to close their positions + // at maturity regardless of whether idle could be distributed. + _distributeExcessIdleSafe(vaultSharePrice); + } // Withdraw the profit to the trader. proceeds = _withdraw(shareProceeds, vaultSharePrice, _options); + // Enforce the minimum user outputs. + // + // NOTE: We use the value that is returned from the withdraw to check + // against the minOutput because in the event of slippage on the + // withdraw, we want it to be caught be the minOutput check. + if (proceeds < _minOutput) { + revert IHyperdrive.OutputLimit(); + } + // Emit a Burn event. emit Burn( msg.sender, @@ -277,10 +283,8 @@ abstract contract HyperdrivePair is uint256 _bondAmount, uint256 _governanceFee ) internal { - // Update the amount of governance fees accrued. Since the long and - // short both pay the governance fee, the governance fees accrued - // increases by twice the governance fee. - _governanceFeesAccrued += 2 * _governanceFee; + // Update the amount of governance fees accrued. + _governanceFeesAccrued += _governanceFee; // Update the average maturity time of longs and short positions and the // amount of long and short positions outstanding. Everything else @@ -309,10 +313,6 @@ abstract contract HyperdrivePair is _marketState.shortsOutstanding += _bondAmount.toUint128(); } - // FIXME: How does this handle negative interest. - // - // FIXME: Natspec - // /// @dev Applies state changes to burn a pair of matched long and short /// positions and release the underlying funds. This operation leaves /// the pool's solvency and idle capital unchanged because the @@ -338,10 +338,8 @@ abstract contract HyperdrivePair is uint256 _bondAmount, uint256 _governanceFee ) internal { - // Update the amount of governance fees accrued. Since the long and - // short both pay the governance fee, the governance fees accrued - // increases by twice the governance fee. - _governanceFeesAccrued += 2 * _governanceFee; + // Update the amount of governance fees accrued. + _governanceFeesAccrued += _governanceFee; // Update the average maturity time of longs and short positions and the // amount of long and short positions outstanding. Everything else @@ -376,6 +374,8 @@ abstract contract HyperdrivePair is /// @param _vaultSharePrice The vault share price. /// @param _openVaultSharePrice The vault share price at the beginning of /// the checkpoint. + /// @return The amount of bonds to mint. + /// @return The governance fee in shares charged to the depositor. function _calculateMint( uint256 _sharesDeposited, uint256 _vaultSharePrice, @@ -416,26 +416,29 @@ abstract contract HyperdrivePair is _flatFee.mulUp(_governanceLPFee)) ); - // The governance fee that will be paid on both the long and the short + // The governance fee that will be paid on the long and the short // sides of the trade in shares is given by: // - // governanceFee = bondAmount * flatFee * governanceLPFee / vaultSharePrice + // governanceFee = 2 * bondAmount * flatFee * governanceLPFee / vaultSharePrice // // NOTE: Round the flat fee calculation up and the governance fee // calculation down to match the rounding used in the other flows. - uint256 governanceFee = bondAmount.mulUp(_flatFee).mulDivDown( - _governanceLPFee, - _vaultSharePrice - ); + uint256 governanceFee = 2 * + bondAmount.mulUp(_flatFee).mulDivDown( + _governanceLPFee, + _vaultSharePrice + ); return (bondAmount, governanceFee); } - // FIXME: How does this work with negative interest? - // - // FIXME: Natspec. - // - // FIXME: Comment on the rounding. + /// @dev Calculates the share proceeds earned and the governance fee from + /// burning the specified amount of bonds. + /// @param _maturityTime The maturity time of the bonds to burn. + /// @param _bondAmount The amount of bonds to burn. + /// @param _vaultSharePrice The vault share price. + /// @return The share proceeds earned from burning the bonds. + /// @return The governance fee in shares charged when burning the bonds. function _calculateBurn( uint256 _maturityTime, uint256 _bondAmount, @@ -453,17 +456,37 @@ abstract contract HyperdrivePair is // The governance fee in shares that will be paid on both the long and // the short sides of the trade is given by: // - // governanceFee = bondAmount * flatFee * governanceLPFee / vaultSharePrice + // governanceFee = 2 * bondAmount * flatFee * governanceLPFee / vaultSharePrice // // NOTE: Round the flat fee calculation up and the governance fee // calculation down to match the rounding used in the other flows. - uint256 governanceFee = _bondAmount.mulUp(_flatFee).mulDivDown( - _governanceLPFee, - _vaultSharePrice - ); + uint256 governanceFee = 2 * + _bondAmount.mulUp(_flatFee).mulDivDown( + _governanceLPFee, + _vaultSharePrice + ); - // FIXME: Double check this accounting. + // If negative interest accrued, the fee amounts need to be scaled. // + // NOTE: Round the fee calculations down when adjusting for negative + // interest. + uint256 openVaultSharePrice = _checkpoints[ + _maturityTime - _positionDuration + ].vaultSharePrice; + uint256 closeVaultSharePrice = block.timestamp < _maturityTime + ? _vaultSharePrice + : _checkpoints[_maturityTime].vaultSharePrice; + if (closeVaultSharePrice < openVaultSharePrice) { + flatFee = flatFee.mulDivDown( + closeVaultSharePrice, + openVaultSharePrice + ); + governanceFee = governanceFee.mulDivDown( + closeVaultSharePrice, + openVaultSharePrice + ); + } + // The total amount of value underlying the longs and shorts in shares // is the face value of the bonds plus the amount of interest that // accrued on the face value. We then add the flat fee to this quantity @@ -472,23 +495,21 @@ abstract contract HyperdrivePair is // by: // // totalValue = (c1 / (c * c0)) * bondAmount + flatFee - 2 * governanceFee - uint256 openVaultSharePrice = _checkpoints[ - _maturityTime - _positionDuration - ].vaultSharePrice; - uint256 closeVaultSharePrice = block.timestamp < _maturityTime - ? _vaultSharePrice - : _checkpoints[_maturityTime].vaultSharePrice; + // + // Since the fees are already scaled for negative interest and the + // `(c1 / (c * c0))` will properly scale the value underlying positions + // for negative interest, this calculation fully supports negative + // interest. + // + // NOTE: Round down to underestimate the share proceeds. uint256 shareProceeds = _bondAmount.mulDivDown( closeVaultSharePrice, _vaultSharePrice.mulDown(openVaultSharePrice) ) + flatFee - - 2 * governanceFee; - // FIXME: Calculate negative interest. Do we want to do this with or - // without fees? - + // Return the share proceeds return (shareProceeds, governanceFee); } } From e748cf9e6f1f97789e7d23ee5c139a00e96347a5 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 3 Jan 2025 22:18:55 -0600 Subject: [PATCH 13/45] Started adding a test suite for the `burn` function --- contracts/src/external/Hyperdrive.sol | 10 + contracts/src/external/HyperdriveTarget4.sol | 21 + contracts/src/interfaces/IHyperdriveCore.sol | 17 + .../src/interfaces/IHyperdriveEvents.sol | 1 + contracts/src/internal/HyperdrivePair.sol | 125 ++- test/units/hyperdrive/BurnTest.t.sol | 951 ++++++++++++++++++ test/units/hyperdrive/CloseLongTest.t.sol | 10 +- test/units/hyperdrive/CloseShortTest.t.sol | 28 +- test/utils/HyperdriveTest.sol | 62 ++ 9 files changed, 1184 insertions(+), 41 deletions(-) create mode 100644 test/units/hyperdrive/BurnTest.t.sol diff --git a/contracts/src/external/Hyperdrive.sol b/contracts/src/external/Hyperdrive.sol index 387f214ef..945376b65 100644 --- a/contracts/src/external/Hyperdrive.sol +++ b/contracts/src/external/Hyperdrive.sol @@ -256,6 +256,16 @@ abstract contract Hyperdrive is _delegate(target4); } + /// @inheritdoc IHyperdriveCore + function burn( + uint256, + uint256, + uint256, + IHyperdrive.Options calldata + ) external returns (uint256) { + _delegate(target4); + } + /// Checkpoints /// /// @inheritdoc IHyperdriveCore diff --git a/contracts/src/external/HyperdriveTarget4.sol b/contracts/src/external/HyperdriveTarget4.sol index e08075544..0f0d76a58 100644 --- a/contracts/src/external/HyperdriveTarget4.sol +++ b/contracts/src/external/HyperdriveTarget4.sol @@ -116,6 +116,27 @@ abstract contract HyperdriveTarget4 is return _mint(_amount, _minOutput, _minVaultSharePrice, _options); } + // FIXME: Where does this fit? + // + /// @dev Burns a pair of long and short positions that directly match each + /// other. The capital underlying these positions is released to the + /// trader burning the positions. + /// @param _maturityTime The maturity time of the long and short positions. + /// @param _bondAmount The amount of longs and shorts to close. + /// @param _minOutput The minimum amount of proceeds to receive. + /// @param _options The options that configure how the trade is settled. + /// @return proceeds The proceeds the user receives. The units of this + /// quantity are either base or vault shares, depending on the value + /// of `_options.asBase`. + function burn( + uint256 _maturityTime, + uint256 _bondAmount, + uint256 _minOutput, + IHyperdrive.Options calldata _options + ) external returns (uint256 proceeds) { + return _burn(_maturityTime, _bondAmount, _minOutput, _options); + } + /// Checkpoints /// /// @notice Allows anyone to mint a new checkpoint. diff --git a/contracts/src/interfaces/IHyperdriveCore.sol b/contracts/src/interfaces/IHyperdriveCore.sol index 659f851eb..2209a95f6 100644 --- a/contracts/src/interfaces/IHyperdriveCore.sol +++ b/contracts/src/interfaces/IHyperdriveCore.sol @@ -186,6 +186,23 @@ interface IHyperdriveCore is IMultiTokenCore { IHyperdrive.PairOptions calldata _options ) external payable returns (uint256 maturityTime, uint256 bondAmount); + /// @dev Burns a pair of long and short positions that directly match each + /// other. The capital underlying these positions is released to the + /// trader burning the positions. + /// @param _maturityTime The maturity time of the long and short positions. + /// @param _bondAmount The amount of longs and shorts to close. + /// @param _minOutput The minimum amount of proceeds to receive. + /// @param _options The options that configure how the trade is settled. + /// @return proceeds The proceeds the user receives. The units of this + /// quantity are either base or vault shares, depending on the value + /// of `_options.asBase`. + function burn( + uint256 _maturityTime, + uint256 _bondAmount, + uint256 _minOutput, + IHyperdrive.Options calldata _options + ) external returns (uint256 proceeds); + /// Checkpoints /// /// @notice Attempts to mint a checkpoint with the specified checkpoint time. diff --git a/contracts/src/interfaces/IHyperdriveEvents.sol b/contracts/src/interfaces/IHyperdriveEvents.sol index e1256b6d0..cb7c60cc2 100644 --- a/contracts/src/interfaces/IHyperdriveEvents.sol +++ b/contracts/src/interfaces/IHyperdriveEvents.sol @@ -119,6 +119,7 @@ interface IHyperdriveEvents is IMultiTokenEvents { /// @notice Emitted when a pair of long and short positions are burned. event Burn( address indexed trader, + address indexed destination, uint256 indexed maturityTime, uint256 longAssetId, uint256 shortAssetId, diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index a14c39f3e..1e92d489b 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -140,6 +140,8 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { return (maturityTime, bondAmount); } + // FIXME: Document the update to the flat fee logic. + // /// @dev Burns a pair of long and short positions that directly match each /// other. The capital underlying these positions is released to the /// trader burning the positions. @@ -200,11 +202,11 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { // Calculate the proceeds of burning the bonds with the specified // maturity. - (uint256 shareProceeds, uint256 governanceFee) = _calculateBurn( - _maturityTime, - _bondAmount, - vaultSharePrice - ); + ( + uint256 shareProceeds, + uint256 flatFee, + uint256 governanceFee + ) = _calculateBurn(_maturityTime, _bondAmount, vaultSharePrice); // If the positions haven't matured, apply the accounting updates that // result from closing the pair to the reserves. @@ -213,11 +215,17 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { // shorts. // // NOTE: Since the spot price doesn't change, we don't update the - // weighted average spot price in this transaction. Similarly, since - // idle doesn't change, we don't distribute excess idle here. It's - // possible that a small amount of interest has accrued, but this - // doesn't warrant the extra gas expenditure. - _applyBurn(_maturityTime, _bondAmount, governanceFee); + // weighted average spot price in this transaction. + _applyBurn(_maturityTime, _bondAmount, flatFee, governanceFee); + + // Distribute the excess idle to the withdrawal pool. If the + // distribute excess idle calculation fails, we revert to avoid + // putting the system in an unhealthy state after the trade is + // processed. + bool success = _distributeExcessIdleSafe(vaultSharePrice); + if (!success) { + revert IHyperdrive.DistributeExcessIdleFailed(); + } } else { // Apply the zombie close to the state and adjust the share proceeds // to account for negative interest that might have accrued to the @@ -244,16 +252,19 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { } // Emit a Burn event. + uint256 bondAmount = _bondAmount; // avoid stack-too-deep + IHyperdrive.Options calldata options = _options; // avoid stack-too-deep emit Burn( msg.sender, + options.destination, _maturityTime, longAssetId, shortAssetId, proceeds, vaultSharePrice, - _options.asBase, - _bondAmount, - _options.extraData + options.asBase, + bondAmount, + options.extraData ); return proceeds; @@ -315,27 +326,30 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { /// @dev Applies state changes to burn a pair of matched long and short /// positions and release the underlying funds. This operation leaves - /// the pool's solvency and idle capital unchanged because the - /// positions fully net out. Specifically: + /// the pool's solvency unchanged because the positions fully net out. + /// Specifically: /// - /// - Share reserves, share adjustments, and bond reserves remain - /// constant since the released capital backs the positions directly. + /// - The share reserves and share adjustment are both increased by the + /// flat fee. Otherwise, the reserves remain constant since the + /// released capital backs the positions directly. /// - Solvency remains constant because the net effect of burning /// matching long and short positions is neutral. - /// - Idle capital is unaffected since no excess funds are added or - /// removed during this process. /// /// Therefore: /// /// - Solvency checks are unnecessary. - /// - Idle capital does not need to be redistributed to LPs. + /// + /// The pool's idle will increase by the flat fees paid and thus idle + /// will need to be distributed. /// @param _maturityTime The maturity time of the pair of long and short /// positions /// @param _bondAmount The amount of bonds burned. - /// @param _governanceFee The governance fee calculated from the bond amount. + /// @param _flatFee The flat fees in shares. + /// @param _governanceFee The governance fees in shares. function _applyBurn( uint256 _maturityTime, uint256 _bondAmount, + uint256 _flatFee, uint256 _governanceFee ) internal { // Update the amount of governance fees accrued. @@ -366,6 +380,10 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { .toUint128(); _marketState.longsOutstanding -= _bondAmount.toUint128(); _marketState.shortsOutstanding -= _bondAmount.toUint128(); + + // Increase the share reserves and the share adjustment by the flat fee. + _marketState.shareReserves += _flatFee.toUint128(); + _marketState.shareAdjustment += _flatFee.toInt128(); } /// @dev Calculates the amount of bonds that can be minted and the governance @@ -432,29 +450,56 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { return (bondAmount, governanceFee); } - /// @dev Calculates the share proceeds earned and the governance fee from - /// burning the specified amount of bonds. + // FIXME: Review the flat fee calculations used in this system. + // + // FIXME: Document the flat fee calculations and clearly explain why we have + // to have them here. + // + /// @dev Calculates the share proceeds earned and the fees from burning the + /// specified amount of bonds. /// @param _maturityTime The maturity time of the bonds to burn. /// @param _bondAmount The amount of bonds to burn. /// @param _vaultSharePrice The vault share price. /// @return The share proceeds earned from burning the bonds. + /// @return The flat fee in shares charged when burning the bonds. /// @return The governance fee in shares charged when burning the bonds. function _calculateBurn( uint256 _maturityTime, uint256 _bondAmount, uint256 _vaultSharePrice - ) internal view returns (uint256, uint256) { + ) internal view returns (uint256, uint256, uint256) { // The short's pre-paid flat fee in shares that will be refunded. This // is given by: // - // flatFee = bondAmount * flatFee / vaultSharePrice + // prepaidFlatFee = bondAmount * flatFee / vaultSharePrice // // NOTE: Round the flat fee calculation up to match the rounding used in // the other flows. - uint256 flatFee = _bondAmount.mulDivUp(_flatFee, _vaultSharePrice); + uint256 timeRemaining = _calculateTimeRemaining(_maturityTime); + uint256 prepaidFlatFee = _bondAmount.mulDivUp( + _flatFee, + _vaultSharePrice + ); - // The governance fee in shares that will be paid on both the long and - // the short sides of the trade is given by: + // Since checkpointing will assume that the flat fee is paid, it's + // simpler to charge the flat fee when burning bonds. This ensures that + // burning is equivalent to redeeming longs and shorts at maturity. The + // governance fees are excluded from this flat fee since the full flat + // governance fee is always paid when burning bonds, regardless of the + // flat fee that is paid. The flat fee is given by: + // + // flatFee = 2 * prepaidFlatFee * (1 - timeRemaining) * (1 - governanceLPFee) + // + // NOTE: Round the flat fee calculation up to match the rounding used in + // the other flows. + uint256 flatFee = 2 * + prepaidFlatFee.mulUp(ONE - timeRemaining).mulDown( + ONE - _governanceLPFee + ); + + // The full flat governance fee is paid whenever bonds are burned. The + // governance fee in shares that will be paid on both the long and the + // short sides of the trade is given by: // // governanceFee = 2 * bondAmount * flatFee * governanceLPFee / vaultSharePrice // @@ -477,6 +522,10 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { ? _vaultSharePrice : _checkpoints[_maturityTime].vaultSharePrice; if (closeVaultSharePrice < openVaultSharePrice) { + prepaidFlatFee = prepaidFlatFee.mulDivDown( + closeVaultSharePrice, + openVaultSharePrice + ); flatFee = flatFee.mulDivDown( closeVaultSharePrice, openVaultSharePrice @@ -491,10 +540,14 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { // is the face value of the bonds plus the amount of interest that // accrued on the face value. We then add the flat fee to this quantity // since this was pre-paid by the short and needs to be refunded. - // Finally, we subtract twice the governance fee. All of this is given - // by: + // Finally, we subtract flat fees and governance fees owed on the bonds. + // The flat fee is pro-rated to the amount of time the bonds have been + // open. All of this is given by: // - // totalValue = (c1 / (c * c0)) * bondAmount + flatFee - 2 * governanceFee + // totalValue = (c1 / (c * c0)) * bondAmount + + // prepaidFlatFee - + // flatFee - + // governancFee // // Since the fees are already scaled for negative interest and the // `(c1 / (c * c0))` will properly scale the value underlying positions @@ -502,14 +555,16 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { // interest. // // NOTE: Round down to underestimate the share proceeds. - uint256 shareProceeds = _bondAmount.mulDivDown( + uint256 bondAmount = _bondAmount; // avoid stack-too-deep + uint256 vaultSharePrice = _vaultSharePrice; // avoid stack-too-deep + uint256 shareProceeds = bondAmount.mulDivDown( closeVaultSharePrice, - _vaultSharePrice.mulDown(openVaultSharePrice) + vaultSharePrice.mulDown(openVaultSharePrice) ) + + prepaidFlatFee - flatFee - governanceFee; - // Return the share proceeds - return (shareProceeds, governanceFee); + return (shareProceeds, flatFee, governanceFee); } } diff --git a/test/units/hyperdrive/BurnTest.t.sol b/test/units/hyperdrive/BurnTest.t.sol new file mode 100644 index 000000000..50b1ebdfa --- /dev/null +++ b/test/units/hyperdrive/BurnTest.t.sol @@ -0,0 +1,951 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.22; + +import { stdError } from "forge-std/StdError.sol"; +import { VmSafe } from "forge-std/Vm.sol"; +import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; +import { AssetId } from "../../../contracts/src/libraries/AssetId.sol"; +import { FixedPointMath, ONE } from "../../../contracts/src/libraries/FixedPointMath.sol"; +import { HyperdriveTest, HyperdriveUtils } from "../../utils/HyperdriveTest.sol"; +import { Lib } from "../../utils/Lib.sol"; + +/// @dev A test suite for the burn function. +contract BurnTest is HyperdriveTest { + using FixedPointMath for uint256; + using HyperdriveUtils for IHyperdrive; + using Lib for *; + + /// @dev Sets up the harness and deploys and initializes a pool with fees. + function setUp() public override { + // Run the higher level setup function. + super.setUp(); + + // Start recording event logs. + vm.recordLogs(); + + // Deploy and initialize a pool with non-trivial fees. The curve fee is + // kept to zero to enable us to compare the result of burning with the + // result of closing the positions separately. + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + POSITION_DURATION + ); + config.fees.flat = 0.0005e18; + config.fees.governanceLP = 0.15e18; + deploy(alice, config); + initialize(alice, 0.05e18, 100_000_000e18); + } + + /// @dev Ensures that burning fails when the amount is zero. + function test_burn_failure_zero_amount() external { + // Mint a set of positions to Bob to use during testing. + uint256 amountPaid = 100_000e18; + (uint256 maturityTime, ) = mint(bob, amountPaid); + + // Attempt to burn with a bond amount of zero. + vm.stopPrank(); + vm.startPrank(bob); + vm.expectRevert(IHyperdrive.MinimumTransactionAmount.selector); + hyperdrive.burn( + maturityTime, + 0, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } + + /// @dev Ensures that burning fails when the destination is the zero address. + function test_burn_failure_destination_zero_address() external { + // Mint a set of positions to Bob to use during testing. + uint256 amountPaid = 100_000e18; + (uint256 maturityTime, uint256 bondAmount) = mint(bob, amountPaid); + + // Alice attempts to set the destination to the zero address. + vm.stopPrank(); + vm.startPrank(alice); + vm.expectRevert(IHyperdrive.RestrictedZeroAddress.selector); + hyperdrive.burn( + maturityTime, + bondAmount, + 0, + IHyperdrive.Options({ + destination: address(0), + asBase: true, + extraData: new bytes(0) + }) + ); + } + + /// @dev Ensures that burning fails when the bond amount is larger than the + /// balance of the burner. + function test_burn_failure_invalid_amount() external { + // Mint a set of positions to Bob to use during testing. + uint256 amountPaid = 100_000e18; + (uint256 maturityTime, uint256 bondAmount) = mint(bob, amountPaid); + + // Attempt to burn too many bonds. This should fail. + vm.stopPrank(); + vm.startPrank(bob); + vm.expectRevert(IHyperdrive.InsufficientBalance.selector); + hyperdrive.burn( + maturityTime, + bondAmount + 1, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } + + /// @dev Ensures that burning fails when the maturity time is zero. + function test_burn_failure_zero_maturity() external { + // Mint a set of positions to Bob to use during testing. + uint256 amountPaid = 100_000e18; + (, uint256 bondAmount) = mint(bob, amountPaid); + + // Attempt to use a maturity time of zero. + vm.stopPrank(); + vm.startPrank(alice); + vm.expectRevert(stdError.arithmeticError); + hyperdrive.burn( + 0, + bondAmount, + 0, + IHyperdrive.Options({ + destination: alice, + asBase: true, + extraData: new bytes(0) + }) + ); + } + + /// @dev Ensures that burning fails when the maturity time is invalid. + function test_burn_failure_invalid_maturity() external { + // Mint a set of positions to Bob to use during testing. + uint256 amountPaid = 100_000e18; + mint(bob, amountPaid); + + // Attempt to use a timestamp greater than the maximum range. + vm.stopPrank(); + vm.startPrank(bob); + vm.expectRevert(IHyperdrive.InvalidTimestamp.selector); + hyperdrive.burn( + uint256(type(uint248).max) + 1, + MINIMUM_TRANSACTION_AMOUNT, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } + + /// @dev Ensures that bonds can be burned successfully immediately after + /// they are minted. + function test_burn_immediately_with_regular_amount() external { + // Mint a set of positions to Alice to use during testing. + uint256 amountPaid = 100_000e18; + (uint256 maturityTime, uint256 bondAmount) = mint(alice, amountPaid); + + // Get some data before minting. + BurnTestCase memory testCase = _burnTestCase( + alice, // burner + bob, // destination + maturityTime, // maturity time + bondAmount, // bond amount + true, // asBase + "" // extraData + ); + + // Verify the mint transaction. + _verifyBurn(testCase, amountPaid); + } + + /// @dev Ensures that a small amount of bonds can be burned successfully + /// immediately after they are minted. + function test_burn_immediately_with_small_amount() external { + // Mint a set of positions to Alice to use during testing. + uint256 amountPaid = 0.01e18; + (uint256 maturityTime, uint256 bondAmount) = mint(alice, amountPaid); + + // Get some data before minting. + BurnTestCase memory testCase = _burnTestCase( + alice, // burner + bob, // destination + maturityTime, // maturity time + bondAmount, // bond amount + true, // asBase + "" // extraData + ); + + // Verify the mint transaction. + _verifyBurn(testCase, amountPaid); + } + + /// @dev Ensure that bonds can be successfully burned halfway through the + /// term. + function test_burn_halfway_through_term() external { + // Alice mints a large pair position. + uint256 amountPaid = 100_000e18; + (uint256 maturityTime, uint256 bondAmount) = mint(alice, amountPaid); + + // Most of the term passes. The variable rate equals the fixed rate. + uint256 timeDelta = 0.5e18; + advanceTime(POSITION_DURATION.mulDown(timeDelta), 0.05e18); + + // Get some data before minting. + BurnTestCase memory testCase = _burnTestCase( + alice, // burner + bob, // destination + maturityTime, // maturity time + bondAmount, // bond amount + true, // asBase + "" // extraData + ); + + // Verify the mint transaction. + _verifyBurn(testCase, amountPaid); + } + + /// @dev Ensure that bonds can be successfully burned at maturity. + function test_burn_at_maturity() external { + // Alice mints a large pair position. + uint256 amountPaid = 100_000e18; + (uint256 maturityTime, uint256 bondAmount) = mint(alice, amountPaid); + + // Most of the term passes. The variable rate equals the fixed rate. + uint256 timeDelta = 1e18; + advanceTime(POSITION_DURATION.mulDown(timeDelta), 0.05e18); + + // Get some data before minting. + BurnTestCase memory testCase = _burnTestCase( + alice, // burner + bob, // destination + maturityTime, // maturity time + bondAmount, // bond amount + true, // asBase + "" // extraData + ); + + // Verify the mint transaction. + _verifyBurn(testCase, amountPaid); + } + + // // FIXME + // function test_close_long_redeem_negative_interest() external { + // // Initialize the pool with a large amount of capital. + // uint256 fixedRate = 0.05e18; + // uint256 contribution = 500_000_000e18; + // initialize(alice, fixedRate, contribution); + + // // Open a long position. + // uint256 basePaid = 10e18; + // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); + + // // Term passes. The pool accrues interest at the current apr. + // uint256 timeAdvanced = POSITION_DURATION; + // int256 apr = -0.3e18; + // advanceTime(timeAdvanced, apr); + + // // Get the reserves before closing the long. + // IHyperdrive.PoolInfo memory poolInfoBefore = hyperdrive.getPoolInfo(); + // uint256 bobBaseBalanceBefore = baseToken.balanceOf(bob); + // uint256 hyperdriveBaseBalanceBefore = baseToken.balanceOf( + // address(hyperdrive) + // ); + + // // Redeem the bonds + // uint256 baseProceeds = closeLong(bob, maturityTime, bondAmount); + + // // Account the negative interest with the bondAmount as principal + // (uint256 bondFaceValue, ) = HyperdriveUtils.calculateCompoundInterest( + // bondAmount, + // apr, + // timeAdvanced + // ); + + // // As negative interest occurred over the duration, the long position + // // takes on the loss. As the "matured" bondAmount is implicitly an + // // amount of shares, the base value of those shares are negative + // // relative to what they were at the start of the term. + // uint256 matureBondsValue = bondAmount + // .divDown(hyperdrive.getPoolConfig().initialVaultSharePrice) + // .mulDown(poolInfoBefore.vaultSharePrice); + + // // Verify that Bob received base equal to the full bond amount. + // assertApproxEqAbs(baseProceeds, bondFaceValue, 10); + // assertApproxEqAbs(baseProceeds, matureBondsValue, 10); + + // // Verify that the close long updates were correct. + // verifyCloseLong( + // TestCase({ + // poolInfoBefore: poolInfoBefore, + // traderBaseBalanceBefore: bobBaseBalanceBefore, + // hyperdriveBaseBalanceBefore: hyperdriveBaseBalanceBefore, + // baseProceeds: baseProceeds, + // bondAmount: bondAmount, + // maturityTime: maturityTime, + // wasCheckpointed: false + // }) + // ); + // } + + // // FIXME + // function test_close_long_half_term_negative_interest() external { + // // Initialize the pool with a large amount of capital. + // uint256 fixedRate = 0.05e18; + // uint256 contribution = 500_000_000e18; + // initialize(alice, fixedRate, contribution); + + // // Open a long position. + // uint256 basePaid = 10e18; + // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); + + // // Term passes. The pool accrues negative interest. + // uint256 timeAdvanced = POSITION_DURATION.mulDown(0.5e18); + // int256 apr = -0.25e18; + // advanceTime(timeAdvanced, apr); + + // // Get the reserves before closing the long. + // IHyperdrive.PoolInfo memory poolInfoBefore = hyperdrive.getPoolInfo(); + // uint256 bobBaseBalanceBefore = baseToken.balanceOf(bob); + // uint256 hyperdriveBaseBalanceBefore = baseToken.balanceOf( + // address(hyperdrive) + // ); + + // // Redeem the bonds + // uint256 baseProceeds = closeLong(bob, maturityTime, bondAmount); + + // // Initial share price + // uint256 initialVaultSharePrice = hyperdrive + // .getPoolConfig() + // .initialVaultSharePrice; + + // // Ensure that the base proceeds are correct. + // { + // // All mature bonds are redeemed at the equivalent amount of shares + // // held throughout the duration, losing capital + // uint256 matureBonds = bondAmount.mulDown( + // ONE - + // HyperdriveUtils.calculateTimeRemaining( + // hyperdrive, + // maturityTime + // ) + // ); + // uint256 bondsValue = matureBonds; + + // // Portion of immature bonds are sold on the YieldSpace curve + // uint256 immatureBonds = bondAmount - matureBonds; + // bondsValue += YieldSpaceMath + // .calculateSharesOutGivenBondsInDown( + // HyperdriveMath.calculateEffectiveShareReserves( + // poolInfoBefore.shareReserves, + // poolInfoBefore.shareAdjustment + // ), + // poolInfoBefore.bondReserves, + // immatureBonds, + // ONE - hyperdrive.getPoolConfig().timeStretch, + // poolInfoBefore.vaultSharePrice, + // initialVaultSharePrice + // ) + // .mulDown(poolInfoBefore.vaultSharePrice); + + // bondsValue = bondsValue.divDown(initialVaultSharePrice).mulDown( + // poolInfoBefore.vaultSharePrice + // ); + + // assertLe(baseProceeds, bondsValue); + // assertApproxEqAbs(baseProceeds, bondsValue, 1); + // } + + // // Verify that the close long updates were correct. + // verifyCloseLong( + // TestCase({ + // poolInfoBefore: poolInfoBefore, + // traderBaseBalanceBefore: bobBaseBalanceBefore, + // hyperdriveBaseBalanceBefore: hyperdriveBaseBalanceBefore, + // baseProceeds: baseProceeds, + // bondAmount: bondAmount, + // maturityTime: maturityTime, + // wasCheckpointed: false + // }) + // ); + // } + + // // FIXME + // // + // // This test ensures that the reserves are updated correctly when longs are + // // closed at maturity with negative interest. + // function test_close_long_negative_interest_at_maturity() external { + // // Initialize the pool with a large amount of capital. + // uint256 fixedRate = 0.05e18; + // uint256 contribution = 500_000_000e18; + // initialize(alice, fixedRate, contribution); + + // // Open a long position. + // uint256 basePaid = 10e18; + // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); + + // // The term passes and the pool accrues negative interest. + // int256 apr = -0.25e18; + // advanceTime(POSITION_DURATION, apr); + + // // Get the reserves and base balances before closing the long. + // IHyperdrive.PoolInfo memory poolInfoBefore = hyperdrive.getPoolInfo(); + // uint256 bobBaseBalanceBefore = baseToken.balanceOf(bob); + // uint256 hyperdriveBaseBalanceBefore = baseToken.balanceOf( + // address(hyperdrive) + // ); + + // // Bob redeems the bonds. Ensure that the return value matches the + // // amount of base transferred to Bob. + // uint256 baseProceeds = closeLong(bob, maturityTime, bondAmount); + // uint256 closeVaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; + + // // Bond holders take a proportional haircut on any negative interest + // // that accrues. + // uint256 bondValue = bondAmount + // .divDown(hyperdrive.getPoolConfig().initialVaultSharePrice) + // .mulDown(closeVaultSharePrice); + + // // Calculate the value of the bonds compounded at the negative APR. + // (uint256 bondFaceValue, ) = HyperdriveUtils.calculateCompoundInterest( + // bondAmount, + // apr, + // POSITION_DURATION + // ); + + // assertApproxEqAbs(baseProceeds, bondValue, 6); + // assertApproxEqAbs(bondValue, bondFaceValue, 5); + + // // Verify that the close long updates were correct. + // verifyCloseLong( + // TestCase({ + // poolInfoBefore: poolInfoBefore, + // traderBaseBalanceBefore: bobBaseBalanceBefore, + // hyperdriveBaseBalanceBefore: hyperdriveBaseBalanceBefore, + // baseProceeds: baseProceeds, + // bondAmount: bondAmount, + // maturityTime: maturityTime, + // wasCheckpointed: false + // }) + // ); + // } + + // // FIXME + // // + // // This test ensures that waiting to close your longs won't avoid negative + // // interest that occurred while the long was open. + // function test_close_long_negative_interest_before_maturity() external { + // // Initialize the pool with a large amount of capital. + // uint256 fixedRate = 0.05e18; + // uint256 contribution = 500_000_000e18; + // initialize(alice, fixedRate, contribution); + + // // Open a long position. + // uint256 basePaid = 10e18; + // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); + + // // The term passes and the pool accrues negative interest. + // int256 apr = -0.25e18; + // advanceTime(POSITION_DURATION, apr); + + // // A checkpoint is created to lock in the close price. + // hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive), 0); + // uint256 closeVaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; + + // // Another term passes and a large amount of positive interest accrues. + // advanceTime(POSITION_DURATION, 0.7e18); + // hyperdrive.checkpoint(hyperdrive.latestCheckpoint(), 0); + + // // Get the reserves and base balances before closing the long. + // IHyperdrive.PoolInfo memory poolInfoBefore = hyperdrive.getPoolInfo(); + // uint256 bobBaseBalanceBefore = baseToken.balanceOf(bob); + // uint256 hyperdriveBaseBalanceBefore = baseToken.balanceOf( + // address(hyperdrive) + // ); + + // // Bob redeems the bonds. Ensure that the return value matches the + // // amount of base transferred to Bob. + // uint256 baseProceeds = closeLong(bob, maturityTime, bondAmount); + + // // Bond holders take a proportional haircut on any negative interest + // // that accrues. + // uint256 bondValue = bondAmount + // .divDown(hyperdrive.getPoolConfig().initialVaultSharePrice) + // .mulDown(closeVaultSharePrice); + + // // Calculate the value of the bonds compounded at the negative APR. + // (uint256 bondFaceValue, ) = HyperdriveUtils.calculateCompoundInterest( + // bondAmount, + // apr, + // POSITION_DURATION + // ); + + // assertLe(baseProceeds, bondValue); + // assertApproxEqAbs(baseProceeds, bondValue, 7); + // assertApproxEqAbs(bondValue, bondFaceValue, 5); + + // // Verify that the close long updates were correct. + // verifyCloseLong( + // TestCase({ + // poolInfoBefore: poolInfoBefore, + // traderBaseBalanceBefore: bobBaseBalanceBefore, + // hyperdriveBaseBalanceBefore: hyperdriveBaseBalanceBefore, + // baseProceeds: baseProceeds, + // bondAmount: bondAmount, + // maturityTime: maturityTime, + // wasCheckpointed: true + // }) + // ); + // } + + // // FIXME + // // + // // This test ensures that waiting to close your longs won't avoid negative + // // interest that occurred after the long was open while it was a zombie. + // function test_close_long_negative_interest_after_maturity() external { + // // Initialize the pool with a large amount of capital. + // uint256 fixedRate = 0.05e18; + // uint256 contribution = 500_000_000e18; + // initialize(alice, fixedRate, contribution); + + // // Open a long position. + // uint256 basePaid = 10e18; + // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); + + // // The term passes and the pool accrues negative interest. + // int256 apr = 0.5e18; + // advanceTime(POSITION_DURATION, apr); + + // // A checkpoint is created to lock in the close price. + // hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive), 0); + // uint256 closeVaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; + + // // Another term passes and a large amount of negative interest accrues. + // int256 negativeApr = -0.2e18; + // advanceTime(POSITION_DURATION, negativeApr); + // hyperdrive.checkpoint(hyperdrive.latestCheckpoint(), 0); + + // // Get the reserves and base balances before closing the long. + // IHyperdrive.PoolInfo memory poolInfoBefore = hyperdrive.getPoolInfo(); + // uint256 bobBaseBalanceBefore = baseToken.balanceOf(bob); + // uint256 hyperdriveBaseBalanceBefore = baseToken.balanceOf( + // address(hyperdrive) + // ); + + // // Bob redeems the bonds. Ensure that the return value matches the + // // amount of base transferred to Bob. + // uint256 baseProceeds = closeLong(bob, maturityTime, bondAmount); + + // // Bond holders take a proportional haircut on any negative interest + // // that accrues. + // uint256 bondValue = bondAmount.divDown(closeVaultSharePrice).mulDown( + // hyperdrive.getPoolInfo().vaultSharePrice + // ); + + // // Calculate the value of the bonds compounded at the negative APR. + // (uint256 bondFaceValue, ) = HyperdriveUtils.calculateCompoundInterest( + // bondAmount, + // negativeApr, + // POSITION_DURATION + // ); + + // assertApproxEqAbs(baseProceeds, bondValue, 6); + // assertApproxEqAbs(bondValue, bondFaceValue, 5); + + // // Verify that the close long updates were correct. + // verifyCloseLong( + // TestCase({ + // poolInfoBefore: poolInfoBefore, + // traderBaseBalanceBefore: bobBaseBalanceBefore, + // hyperdriveBaseBalanceBefore: hyperdriveBaseBalanceBefore, + // baseProceeds: baseProceeds, + // bondAmount: bondAmount, + // maturityTime: maturityTime, + // wasCheckpointed: true + // }) + // ); + // } + + // // FIXME + // function test_close_long_after_matured_long() external { + // // Initialize the pool with a large amount of capital. + // uint256 fixedRate = 0.05e18; + // uint256 contribution = 500_000_000e18; + // initialize(alice, fixedRate, contribution); + + // // A large long is opened and held until maturity. This should decrease + // // the share adjustment by the long amount. + // int256 shareAdjustmentBefore = hyperdrive.getPoolInfo().shareAdjustment; + // (, uint256 longAmount) = openLong( + // celine, + // hyperdrive.calculateMaxLong() / 2 + // ); + // advanceTime(hyperdrive.getPoolConfig().positionDuration, 0); + // hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive), 0); + // assertEq( + // hyperdrive.getPoolInfo().shareAdjustment, + // shareAdjustmentBefore - int256(longAmount) + // ); + + // // Bob opens a small long. + // uint256 basePaid = 1_000_000e18; + // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); + + // // Celine opens a large short. This will make it harder for Bob to close + // // his long (however there should be adequate liquidity left). + // openShort(celine, hyperdrive.calculateMaxShort() / 2); + + // // Bob is able to close his long. + // closeLong(bob, maturityTime, bondAmount); + // } + + // // FIXME + // // + // // Test that the close long function works correctly after a matured short + // // is closed. + // function test_close_long_after_matured_short() external { + // // Initialize the pool with a large amount of capital. + // uint256 fixedRate = 0.05e18; + // uint256 contribution = 500_000_000e18; + // initialize(alice, fixedRate, contribution); + + // // A large short is opened and held until maturity. This should increase + // // the share adjustment by the short amount. + // int256 shareAdjustmentBefore = hyperdrive.getPoolInfo().shareAdjustment; + // uint256 shortAmount = hyperdrive.calculateMaxShort() / 2; + // openShort(celine, shortAmount); + // advanceTime(hyperdrive.getPoolConfig().positionDuration, 0); + // hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive), 0); + // assertEq( + // hyperdrive.getPoolInfo().shareAdjustment, + // shareAdjustmentBefore + int256(shortAmount) + // ); + + // // Bob opens a small long. + // uint256 basePaid = 1_000_000e18; + // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); + + // // Celine opens a large short. This will make it harder for Bob to close + // // his long (however there should be adequate liquidity left). + // openShort(celine, hyperdrive.calculateMaxShort() / 2); + + // // Bob is able to close his long. + // closeLong(bob, maturityTime, bondAmount); + // } + + struct BurnTestCase { + // Trading metadata. + address burner; + address destination; + uint256 maturityTime; + uint256 bondAmount; + bool asBase; + bytes extraData; + // The balances before the mint. + uint256 burnerLongBalanceBefore; + uint256 burnerShortBalanceBefore; + uint256 destinationBaseBalanceBefore; + uint256 hyperdriveBaseBalanceBefore; + // The state variables before the mint. + uint256 longsOutstandingBefore; + uint256 shortsOutstandingBefore; + uint256 governanceFeesAccruedBefore; + // Idle, pool depth, and spot price before the mint. + uint256 idleBefore; + uint256 kBefore; + uint256 spotPriceBefore; + uint256 lpSharePriceBefore; + } + + /// @dev Creates the test case for the burn transaction. + /// @param _burner The owner of the bonds to burn. + /// @param _destination The destination of the proceeds. + /// @param _maturityTime The maturity time of the bonds to burn. + /// @param _bondAmount The amount of bonds to burn. + /// @param _asBase A flag indicating whether or not the deposit is in base + /// or vault shares. + /// @param _extraData The extra data for the transaction. + function _burnTestCase( + address _burner, + address _destination, + uint256 _maturityTime, + uint256 _bondAmount, + bool _asBase, + bytes memory _extraData + ) internal view returns (BurnTestCase memory) { + return + BurnTestCase({ + // Trading metadata. + burner: _burner, + destination: _destination, + maturityTime: _maturityTime, + bondAmount: _bondAmount, + asBase: _asBase, + extraData: _extraData, + // The balances before the burn. + burnerLongBalanceBefore: hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + _maturityTime + ), + _burner + ), + burnerShortBalanceBefore: hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + _maturityTime + ), + _burner + ), + destinationBaseBalanceBefore: baseToken.balanceOf(_destination), + hyperdriveBaseBalanceBefore: baseToken.balanceOf( + address(hyperdrive) + ), + // The state variables before the mint. + longsOutstandingBefore: hyperdrive + .getPoolInfo() + .longsOutstanding, + shortsOutstandingBefore: hyperdrive + .getPoolInfo() + .shortsOutstanding, + governanceFeesAccruedBefore: hyperdrive + .getUncollectedGovernanceFees(), + // Idle, pool depth, and spot price before the mint. + idleBefore: hyperdrive.idle(), + kBefore: hyperdrive.k(), + spotPriceBefore: hyperdrive.calculateSpotPrice(), + lpSharePriceBefore: hyperdrive.getPoolInfo().lpSharePrice + }); + } + + /// @dev Process a burn transaction and verify that the state was updated + /// correctly. + /// @param _testCase The test case for the burn test. + /// @param _amountPaid The amount paid for the mint. + function _verifyBurn( + BurnTestCase memory _testCase, + uint256 _amountPaid + ) internal { + // Before burning the bonds, close the long and short separately using + // `closeLong` and `closeShort` in a snapshot. The combined proceeds + // should be approximately equal to the proceeds of the burn. + uint256 expectedProceeds; + { + uint256 snapshotId = vm.snapshot(); + expectedProceeds += closeLong( + _testCase.burner, + _testCase.maturityTime, + _testCase.bondAmount, + _testCase.asBase + ); + expectedProceeds += closeShort( + _testCase.burner, + _testCase.maturityTime, + _testCase.bondAmount, + _testCase.asBase + ); + vm.revertTo(snapshotId); + } + + // Ensure that the burner can successfully burn the tokens. + vm.stopPrank(); + vm.startPrank(_testCase.burner); + uint256 proceeds = hyperdrive.burn( + _testCase.maturityTime, + _testCase.bondAmount, + 0, + IHyperdrive.Options({ + destination: _testCase.destination, + asBase: _testCase.asBase, + extraData: _testCase.extraData + }) + ); + + // If no interest or negative interest accrued, ensure that the proceeds + // were less than the amount deposited. + uint256 openVaultSharePrice = hyperdrive + .getCheckpoint( + _testCase.maturityTime - + hyperdrive.getPoolConfig().positionDuration + ) + .vaultSharePrice; + if (hyperdrive.getPoolInfo().vaultSharePrice <= openVaultSharePrice) { + assertLe(proceeds, _amountPaid); + } + + // Ensure that the proceeds closely match the expected proceeds. We need + // to adjust expected proceeds so that the governance fees paid match + // those paid during the burn. Burning bonds always costs twice the flat + // governance fee whereas closing positions costs a combination of curve + // and flat governance fees. + uint256 governanceFeeAdjustment = 2 * + _testCase + .bondAmount + .mulUp(hyperdrive.getPoolConfig().fees.flat) + .mulDown( + hyperdrive.calculateTimeRemaining(_testCase.maturityTime) + ) + .mulDown(hyperdrive.getPoolConfig().fees.governanceLP); + assertApproxEqAbs( + proceeds + governanceFeeAdjustment, + expectedProceeds, + 1e10 + ); + + // Verify that the balances increased and decreased by the right amounts. + assertEq( + baseToken.balanceOf(_testCase.destination), + _testCase.destinationBaseBalanceBefore + proceeds + ); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + _testCase.maturityTime + ), + _testCase.burner + ), + _testCase.burnerLongBalanceBefore - _testCase.bondAmount + ); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + _testCase.maturityTime + ), + _testCase.burner + ), + _testCase.burnerShortBalanceBefore - _testCase.bondAmount + ); + assertEq( + baseToken.balanceOf(address(hyperdrive)), + _testCase.hyperdriveBaseBalanceBefore - proceeds + ); + + // Verify that the pool's idle increased by the flat fee minus the + // governance fees. + uint256 flatFee = 2 * + _testCase + .bondAmount + .mulUp(hyperdrive.getPoolConfig().fees.flat) + .mulDown( + ONE - + hyperdrive.calculateTimeRemaining( + _testCase.maturityTime + ) + ) + .mulDown(ONE - hyperdrive.getPoolConfig().fees.governanceLP); + assertApproxEqAbs( + hyperdrive.idle(), + _testCase.idleBefore + flatFee, + 10 + ); + + // If the flat fee is zero, ensure that the LP share price was unchanged. + if (flatFee == 0) { + assertApproxEqAbs( + hyperdrive.getPoolInfo().lpSharePrice, + _testCase.lpSharePriceBefore, + 1 + ); + } + // Otherwise, ensure that the LP share price increased. + else { + assertGt( + hyperdrive.getPoolInfo().lpSharePrice, + _testCase.lpSharePriceBefore + ); + } + + // Verify that spot price and pool depth are unchanged. + assertEq(hyperdrive.calculateSpotPrice(), _testCase.spotPriceBefore); + assertEq(hyperdrive.k(), _testCase.kBefore); + + // Ensure that the longs outstanding, shorts outstanding, and governance + // fees accrued decreased by the right amount. + assertEq( + hyperdrive.getPoolInfo().longsOutstanding, + _testCase.longsOutstandingBefore - _testCase.bondAmount + ); + assertEq( + hyperdrive.getPoolInfo().shortsOutstanding, + _testCase.shortsOutstandingBefore - _testCase.bondAmount + ); + assertEq( + hyperdrive.getUncollectedGovernanceFees(), + _testCase.governanceFeesAccruedBefore + + 2 * + _testCase + .bondAmount + .mulUp(hyperdrive.getPoolConfig().fees.flat) + .mulDivDown( + hyperdrive.getPoolConfig().fees.governanceLP, + hyperdrive.getPoolInfo().vaultSharePrice + ) + ); + + // Verify the `Burn` event. + _verifyBurnEvent(_testCase, proceeds); + } + + /// @dev Verify the burn event. + /// @param _testCase The test case containing all of the metadata and data + /// relating to the burn transaction. + /// @param _proceeds The proceeds of burning the bonds. + function _verifyBurnEvent( + BurnTestCase memory _testCase, + uint256 _proceeds + ) internal { + VmSafe.Log[] memory logs = vm.getRecordedLogs().filterLogs( + Burn.selector + ); + assertEq(logs.length, 1); + VmSafe.Log memory log = logs[0]; + assertEq(address(uint160(uint256(log.topics[1]))), _testCase.burner); + assertEq( + address(uint160(uint256(log.topics[2]))), + _testCase.destination + ); + assertEq(uint256(log.topics[3]), _testCase.maturityTime); + ( + uint256 longAssetId, + uint256 shortAssetId, + uint256 amount, + uint256 vaultSharePrice, + bool asBase, + uint256 bondAmount, + bytes memory extraData + ) = abi.decode( + log.data, + (uint256, uint256, uint256, uint256, bool, uint256, bytes) + ); + assertEq( + longAssetId, + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + _testCase.maturityTime + ) + ); + assertEq( + shortAssetId, + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + _testCase.maturityTime + ) + ); + assertEq(amount, _proceeds); + assertEq(vaultSharePrice, hyperdrive.getPoolInfo().vaultSharePrice); + assertEq(asBase, _testCase.asBase); + assertEq(bondAmount, _testCase.bondAmount); + assertEq(extraData, _testCase.extraData); + } +} diff --git a/test/units/hyperdrive/CloseLongTest.t.sol b/test/units/hyperdrive/CloseLongTest.t.sol index dd4090aab..085bc591d 100644 --- a/test/units/hyperdrive/CloseLongTest.t.sol +++ b/test/units/hyperdrive/CloseLongTest.t.sol @@ -106,19 +106,19 @@ contract CloseLongTest is HyperdriveTest { // Initialize the pool with a large amount of capital. uint256 fixedRate = 0.05e18; uint256 contribution = 500_000_000e18; - uint256 lpShares = initialize(alice, fixedRate, contribution); + initialize(alice, fixedRate, contribution); // Open a long position. uint256 baseAmount = 30e18; - openLong(bob, baseAmount); + (, uint256 bondAmount) = openLong(bob, baseAmount); - // Attempt to use a timestamp greater than the maximum range. + // Attempt to use a zero timestamp. vm.stopPrank(); vm.startPrank(alice); vm.expectRevert(stdError.arithmeticError); hyperdrive.closeLong( 0, - lpShares, + bondAmount, 0, IHyperdrive.Options({ destination: alice, @@ -128,7 +128,7 @@ contract CloseLongTest is HyperdriveTest { ); } - function test_close_long_failure_invalid_timestamp() external { + function test_close_long_failure_invalid_maturity() external { // Initialize the pool with a large amount of capital. uint256 fixedRate = 0.05e18; uint256 contribution = 500_000_000e18; diff --git a/test/units/hyperdrive/CloseShortTest.t.sol b/test/units/hyperdrive/CloseShortTest.t.sol index a6fd8dd63..a4e623306 100644 --- a/test/units/hyperdrive/CloseShortTest.t.sol +++ b/test/units/hyperdrive/CloseShortTest.t.sol @@ -101,7 +101,33 @@ contract CloseShortTest is HyperdriveTest { ); } - function test_close_short_failure_invalid_timestamp() external { + function test_close_short_failure_zero_maturity() external { + // Initialize the pool with a large amount of capital. + uint256 fixedRate = 0.05e18; + uint256 contribution = 500_000_000e18; + initialize(alice, fixedRate, contribution); + + // Open a short position. + uint256 bondAmount = 30e18; + openShort(bob, bondAmount); + + // Attempt to use a zero timestamp. + vm.stopPrank(); + vm.startPrank(alice); + vm.expectRevert(stdError.arithmeticError); + hyperdrive.closeShort( + 0, + bondAmount, + 0, + IHyperdrive.Options({ + destination: alice, + asBase: true, + extraData: new bytes(0) + }) + ); + } + + function test_close_short_failure_invalid_maturity() external { // Initialize the pool with a large amount of capital. uint256 apr = 0.05e18; uint256 contribution = 500_000_000e18; diff --git a/test/utils/HyperdriveTest.sol b/test/utils/HyperdriveTest.sol index f0f59773c..164569c17 100644 --- a/test/utils/HyperdriveTest.sol +++ b/test/utils/HyperdriveTest.sol @@ -944,6 +944,68 @@ contract HyperdriveTest is IHyperdriveEvents, BaseTest { ); } + function burn( + address trader, + uint256 maturityTime, + uint256 bondAmount, + WithdrawalOverrides memory overrides + ) internal returns (uint256 baseAmount) { + vm.stopPrank(); + vm.startPrank(trader); + + // Burn the bonds. + return + hyperdrive.burn( + maturityTime, + bondAmount, + overrides.minSlippage, // min base proceeds + IHyperdrive.Options({ + destination: overrides.destination, + asBase: overrides.asBase, + extraData: overrides.extraData + }) + ); + } + + function burn( + address trader, + uint256 maturityTime, + uint256 bondAmount + ) internal returns (uint256 baseAmount) { + return + burn( + trader, + maturityTime, + bondAmount, + WithdrawalOverrides({ + asBase: true, + destination: trader, + minSlippage: 0, // min base proceeds of 0 + extraData: new bytes(0) // unused + }) + ); + } + + function burn( + address trader, + uint256 maturityTime, + uint256 bondAmount, + bool asBase + ) internal returns (uint256 baseAmount) { + return + burn( + trader, + maturityTime, + bondAmount, + WithdrawalOverrides({ + asBase: asBase, + destination: trader, + minSlippage: 0, // min base proceeds of 0 + extraData: new bytes(0) // unused + }) + ); + } + /// Utils /// function advanceTime(uint256 time, int256 variableRate) internal virtual { From 788ea8bbee64026591ce73f346beff23d9eff88f Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 3 Jan 2025 22:41:29 -0600 Subject: [PATCH 14/45] Added the remaining test cases. Some of them are broken. --- test/units/hyperdrive/BurnTest.t.sol | 534 ++++++--------------------- 1 file changed, 117 insertions(+), 417 deletions(-) diff --git a/test/units/hyperdrive/BurnTest.t.sol b/test/units/hyperdrive/BurnTest.t.sol index 50b1ebdfa..c1024cb5e 100644 --- a/test/units/hyperdrive/BurnTest.t.sol +++ b/test/units/hyperdrive/BurnTest.t.sol @@ -238,409 +238,87 @@ contract BurnTest is HyperdriveTest { _verifyBurn(testCase, amountPaid); } - // // FIXME - // function test_close_long_redeem_negative_interest() external { - // // Initialize the pool with a large amount of capital. - // uint256 fixedRate = 0.05e18; - // uint256 contribution = 500_000_000e18; - // initialize(alice, fixedRate, contribution); - - // // Open a long position. - // uint256 basePaid = 10e18; - // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); - - // // Term passes. The pool accrues interest at the current apr. - // uint256 timeAdvanced = POSITION_DURATION; - // int256 apr = -0.3e18; - // advanceTime(timeAdvanced, apr); - - // // Get the reserves before closing the long. - // IHyperdrive.PoolInfo memory poolInfoBefore = hyperdrive.getPoolInfo(); - // uint256 bobBaseBalanceBefore = baseToken.balanceOf(bob); - // uint256 hyperdriveBaseBalanceBefore = baseToken.balanceOf( - // address(hyperdrive) - // ); - - // // Redeem the bonds - // uint256 baseProceeds = closeLong(bob, maturityTime, bondAmount); - - // // Account the negative interest with the bondAmount as principal - // (uint256 bondFaceValue, ) = HyperdriveUtils.calculateCompoundInterest( - // bondAmount, - // apr, - // timeAdvanced - // ); - - // // As negative interest occurred over the duration, the long position - // // takes on the loss. As the "matured" bondAmount is implicitly an - // // amount of shares, the base value of those shares are negative - // // relative to what they were at the start of the term. - // uint256 matureBondsValue = bondAmount - // .divDown(hyperdrive.getPoolConfig().initialVaultSharePrice) - // .mulDown(poolInfoBefore.vaultSharePrice); - - // // Verify that Bob received base equal to the full bond amount. - // assertApproxEqAbs(baseProceeds, bondFaceValue, 10); - // assertApproxEqAbs(baseProceeds, matureBondsValue, 10); - - // // Verify that the close long updates were correct. - // verifyCloseLong( - // TestCase({ - // poolInfoBefore: poolInfoBefore, - // traderBaseBalanceBefore: bobBaseBalanceBefore, - // hyperdriveBaseBalanceBefore: hyperdriveBaseBalanceBefore, - // baseProceeds: baseProceeds, - // bondAmount: bondAmount, - // maturityTime: maturityTime, - // wasCheckpointed: false - // }) - // ); - // } - - // // FIXME - // function test_close_long_half_term_negative_interest() external { - // // Initialize the pool with a large amount of capital. - // uint256 fixedRate = 0.05e18; - // uint256 contribution = 500_000_000e18; - // initialize(alice, fixedRate, contribution); - - // // Open a long position. - // uint256 basePaid = 10e18; - // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); - - // // Term passes. The pool accrues negative interest. - // uint256 timeAdvanced = POSITION_DURATION.mulDown(0.5e18); - // int256 apr = -0.25e18; - // advanceTime(timeAdvanced, apr); - - // // Get the reserves before closing the long. - // IHyperdrive.PoolInfo memory poolInfoBefore = hyperdrive.getPoolInfo(); - // uint256 bobBaseBalanceBefore = baseToken.balanceOf(bob); - // uint256 hyperdriveBaseBalanceBefore = baseToken.balanceOf( - // address(hyperdrive) - // ); - - // // Redeem the bonds - // uint256 baseProceeds = closeLong(bob, maturityTime, bondAmount); - - // // Initial share price - // uint256 initialVaultSharePrice = hyperdrive - // .getPoolConfig() - // .initialVaultSharePrice; - - // // Ensure that the base proceeds are correct. - // { - // // All mature bonds are redeemed at the equivalent amount of shares - // // held throughout the duration, losing capital - // uint256 matureBonds = bondAmount.mulDown( - // ONE - - // HyperdriveUtils.calculateTimeRemaining( - // hyperdrive, - // maturityTime - // ) - // ); - // uint256 bondsValue = matureBonds; - - // // Portion of immature bonds are sold on the YieldSpace curve - // uint256 immatureBonds = bondAmount - matureBonds; - // bondsValue += YieldSpaceMath - // .calculateSharesOutGivenBondsInDown( - // HyperdriveMath.calculateEffectiveShareReserves( - // poolInfoBefore.shareReserves, - // poolInfoBefore.shareAdjustment - // ), - // poolInfoBefore.bondReserves, - // immatureBonds, - // ONE - hyperdrive.getPoolConfig().timeStretch, - // poolInfoBefore.vaultSharePrice, - // initialVaultSharePrice - // ) - // .mulDown(poolInfoBefore.vaultSharePrice); - - // bondsValue = bondsValue.divDown(initialVaultSharePrice).mulDown( - // poolInfoBefore.vaultSharePrice - // ); - - // assertLe(baseProceeds, bondsValue); - // assertApproxEqAbs(baseProceeds, bondsValue, 1); - // } - - // // Verify that the close long updates were correct. - // verifyCloseLong( - // TestCase({ - // poolInfoBefore: poolInfoBefore, - // traderBaseBalanceBefore: bobBaseBalanceBefore, - // hyperdriveBaseBalanceBefore: hyperdriveBaseBalanceBefore, - // baseProceeds: baseProceeds, - // bondAmount: bondAmount, - // maturityTime: maturityTime, - // wasCheckpointed: false - // }) - // ); - // } - - // // FIXME - // // - // // This test ensures that the reserves are updated correctly when longs are - // // closed at maturity with negative interest. - // function test_close_long_negative_interest_at_maturity() external { - // // Initialize the pool with a large amount of capital. - // uint256 fixedRate = 0.05e18; - // uint256 contribution = 500_000_000e18; - // initialize(alice, fixedRate, contribution); - - // // Open a long position. - // uint256 basePaid = 10e18; - // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); - - // // The term passes and the pool accrues negative interest. - // int256 apr = -0.25e18; - // advanceTime(POSITION_DURATION, apr); - - // // Get the reserves and base balances before closing the long. - // IHyperdrive.PoolInfo memory poolInfoBefore = hyperdrive.getPoolInfo(); - // uint256 bobBaseBalanceBefore = baseToken.balanceOf(bob); - // uint256 hyperdriveBaseBalanceBefore = baseToken.balanceOf( - // address(hyperdrive) - // ); - - // // Bob redeems the bonds. Ensure that the return value matches the - // // amount of base transferred to Bob. - // uint256 baseProceeds = closeLong(bob, maturityTime, bondAmount); - // uint256 closeVaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; - - // // Bond holders take a proportional haircut on any negative interest - // // that accrues. - // uint256 bondValue = bondAmount - // .divDown(hyperdrive.getPoolConfig().initialVaultSharePrice) - // .mulDown(closeVaultSharePrice); - - // // Calculate the value of the bonds compounded at the negative APR. - // (uint256 bondFaceValue, ) = HyperdriveUtils.calculateCompoundInterest( - // bondAmount, - // apr, - // POSITION_DURATION - // ); - - // assertApproxEqAbs(baseProceeds, bondValue, 6); - // assertApproxEqAbs(bondValue, bondFaceValue, 5); - - // // Verify that the close long updates were correct. - // verifyCloseLong( - // TestCase({ - // poolInfoBefore: poolInfoBefore, - // traderBaseBalanceBefore: bobBaseBalanceBefore, - // hyperdriveBaseBalanceBefore: hyperdriveBaseBalanceBefore, - // baseProceeds: baseProceeds, - // bondAmount: bondAmount, - // maturityTime: maturityTime, - // wasCheckpointed: false - // }) - // ); - // } - - // // FIXME - // // - // // This test ensures that waiting to close your longs won't avoid negative - // // interest that occurred while the long was open. - // function test_close_long_negative_interest_before_maturity() external { - // // Initialize the pool with a large amount of capital. - // uint256 fixedRate = 0.05e18; - // uint256 contribution = 500_000_000e18; - // initialize(alice, fixedRate, contribution); - - // // Open a long position. - // uint256 basePaid = 10e18; - // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); - - // // The term passes and the pool accrues negative interest. - // int256 apr = -0.25e18; - // advanceTime(POSITION_DURATION, apr); - - // // A checkpoint is created to lock in the close price. - // hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive), 0); - // uint256 closeVaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; - - // // Another term passes and a large amount of positive interest accrues. - // advanceTime(POSITION_DURATION, 0.7e18); - // hyperdrive.checkpoint(hyperdrive.latestCheckpoint(), 0); - - // // Get the reserves and base balances before closing the long. - // IHyperdrive.PoolInfo memory poolInfoBefore = hyperdrive.getPoolInfo(); - // uint256 bobBaseBalanceBefore = baseToken.balanceOf(bob); - // uint256 hyperdriveBaseBalanceBefore = baseToken.balanceOf( - // address(hyperdrive) - // ); - - // // Bob redeems the bonds. Ensure that the return value matches the - // // amount of base transferred to Bob. - // uint256 baseProceeds = closeLong(bob, maturityTime, bondAmount); - - // // Bond holders take a proportional haircut on any negative interest - // // that accrues. - // uint256 bondValue = bondAmount - // .divDown(hyperdrive.getPoolConfig().initialVaultSharePrice) - // .mulDown(closeVaultSharePrice); - - // // Calculate the value of the bonds compounded at the negative APR. - // (uint256 bondFaceValue, ) = HyperdriveUtils.calculateCompoundInterest( - // bondAmount, - // apr, - // POSITION_DURATION - // ); - - // assertLe(baseProceeds, bondValue); - // assertApproxEqAbs(baseProceeds, bondValue, 7); - // assertApproxEqAbs(bondValue, bondFaceValue, 5); - - // // Verify that the close long updates were correct. - // verifyCloseLong( - // TestCase({ - // poolInfoBefore: poolInfoBefore, - // traderBaseBalanceBefore: bobBaseBalanceBefore, - // hyperdriveBaseBalanceBefore: hyperdriveBaseBalanceBefore, - // baseProceeds: baseProceeds, - // bondAmount: bondAmount, - // maturityTime: maturityTime, - // wasCheckpointed: true - // }) - // ); - // } - - // // FIXME - // // - // // This test ensures that waiting to close your longs won't avoid negative - // // interest that occurred after the long was open while it was a zombie. - // function test_close_long_negative_interest_after_maturity() external { - // // Initialize the pool with a large amount of capital. - // uint256 fixedRate = 0.05e18; - // uint256 contribution = 500_000_000e18; - // initialize(alice, fixedRate, contribution); - - // // Open a long position. - // uint256 basePaid = 10e18; - // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); - - // // The term passes and the pool accrues negative interest. - // int256 apr = 0.5e18; - // advanceTime(POSITION_DURATION, apr); - - // // A checkpoint is created to lock in the close price. - // hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive), 0); - // uint256 closeVaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; - - // // Another term passes and a large amount of negative interest accrues. - // int256 negativeApr = -0.2e18; - // advanceTime(POSITION_DURATION, negativeApr); - // hyperdrive.checkpoint(hyperdrive.latestCheckpoint(), 0); - - // // Get the reserves and base balances before closing the long. - // IHyperdrive.PoolInfo memory poolInfoBefore = hyperdrive.getPoolInfo(); - // uint256 bobBaseBalanceBefore = baseToken.balanceOf(bob); - // uint256 hyperdriveBaseBalanceBefore = baseToken.balanceOf( - // address(hyperdrive) - // ); - - // // Bob redeems the bonds. Ensure that the return value matches the - // // amount of base transferred to Bob. - // uint256 baseProceeds = closeLong(bob, maturityTime, bondAmount); - - // // Bond holders take a proportional haircut on any negative interest - // // that accrues. - // uint256 bondValue = bondAmount.divDown(closeVaultSharePrice).mulDown( - // hyperdrive.getPoolInfo().vaultSharePrice - // ); - - // // Calculate the value of the bonds compounded at the negative APR. - // (uint256 bondFaceValue, ) = HyperdriveUtils.calculateCompoundInterest( - // bondAmount, - // negativeApr, - // POSITION_DURATION - // ); - - // assertApproxEqAbs(baseProceeds, bondValue, 6); - // assertApproxEqAbs(bondValue, bondFaceValue, 5); - - // // Verify that the close long updates were correct. - // verifyCloseLong( - // TestCase({ - // poolInfoBefore: poolInfoBefore, - // traderBaseBalanceBefore: bobBaseBalanceBefore, - // hyperdriveBaseBalanceBefore: hyperdriveBaseBalanceBefore, - // baseProceeds: baseProceeds, - // bondAmount: bondAmount, - // maturityTime: maturityTime, - // wasCheckpointed: true - // }) - // ); - // } - - // // FIXME - // function test_close_long_after_matured_long() external { - // // Initialize the pool with a large amount of capital. - // uint256 fixedRate = 0.05e18; - // uint256 contribution = 500_000_000e18; - // initialize(alice, fixedRate, contribution); - - // // A large long is opened and held until maturity. This should decrease - // // the share adjustment by the long amount. - // int256 shareAdjustmentBefore = hyperdrive.getPoolInfo().shareAdjustment; - // (, uint256 longAmount) = openLong( - // celine, - // hyperdrive.calculateMaxLong() / 2 - // ); - // advanceTime(hyperdrive.getPoolConfig().positionDuration, 0); - // hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive), 0); - // assertEq( - // hyperdrive.getPoolInfo().shareAdjustment, - // shareAdjustmentBefore - int256(longAmount) - // ); - - // // Bob opens a small long. - // uint256 basePaid = 1_000_000e18; - // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); - - // // Celine opens a large short. This will make it harder for Bob to close - // // his long (however there should be adequate liquidity left). - // openShort(celine, hyperdrive.calculateMaxShort() / 2); - - // // Bob is able to close his long. - // closeLong(bob, maturityTime, bondAmount); - // } - - // // FIXME - // // - // // Test that the close long function works correctly after a matured short - // // is closed. - // function test_close_long_after_matured_short() external { - // // Initialize the pool with a large amount of capital. - // uint256 fixedRate = 0.05e18; - // uint256 contribution = 500_000_000e18; - // initialize(alice, fixedRate, contribution); - - // // A large short is opened and held until maturity. This should increase - // // the share adjustment by the short amount. - // int256 shareAdjustmentBefore = hyperdrive.getPoolInfo().shareAdjustment; - // uint256 shortAmount = hyperdrive.calculateMaxShort() / 2; - // openShort(celine, shortAmount); - // advanceTime(hyperdrive.getPoolConfig().positionDuration, 0); - // hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive), 0); - // assertEq( - // hyperdrive.getPoolInfo().shareAdjustment, - // shareAdjustmentBefore + int256(shortAmount) - // ); - - // // Bob opens a small long. - // uint256 basePaid = 1_000_000e18; - // (uint256 maturityTime, uint256 bondAmount) = openLong(bob, basePaid); - - // // Celine opens a large short. This will make it harder for Bob to close - // // his long (however there should be adequate liquidity left). - // openShort(celine, hyperdrive.calculateMaxShort() / 2); - - // // Bob is able to close his long. - // closeLong(bob, maturityTime, bondAmount); - // } + // FIXME: Figure out why this test is failing. + // + /// @dev Ensure that bonds can be burned successfully halfway through the + /// term after negative interest accrues. + function test_burn_halfway_through_term_negative_interest() external { + // Alice mints a large pair position. + uint256 amountPaid = 100_000e18; + (uint256 maturityTime, uint256 bondAmount) = mint(alice, amountPaid); + + // Half of the term passes. A significant amount of negative interest + // accrues. + uint256 timeDelta = 0.5e18; + advanceTime(POSITION_DURATION.mulDown(timeDelta), -0.3e18); + + // Get some data before minting. + BurnTestCase memory testCase = _burnTestCase( + alice, // burner + bob, // destination + maturityTime, // maturity time + bondAmount, // bond amount + true, // asBase + "" // extraData + ); + + // Verify the mint transaction. + _verifyBurn(testCase, amountPaid); + } + + /// @dev Ensure that bonds can be burned successfully at maturity after + /// negative interest accrues. + function test_burn_at_maturity_negative_interest() external { + // Alice mints a large pair position. + uint256 amountPaid = 100_000e18; + (uint256 maturityTime, uint256 bondAmount) = mint(alice, amountPaid); + + // The term passes. A significant amount of negative interest + // accrues. + uint256 timeDelta = 1e18; + advanceTime(POSITION_DURATION.mulDown(timeDelta), -0.3e18); + + // Get some data before minting. + BurnTestCase memory testCase = _burnTestCase( + alice, // burner + bob, // destination + maturityTime, // maturity time + bondAmount, // bond amount + true, // asBase + "" // extraData + ); + + // Verify the mint transaction. + _verifyBurn(testCase, amountPaid); + } + + // FIXME: Figure out why this test is failing. + // + /// @dev Ensure that bonds can be burned successfully after maturity after + /// negative interest accrues. + function test_burn_after_maturity_negative_interest() external { + // Alice mints a large pair position. + uint256 amountPaid = 100_000e18; + (uint256 maturityTime, uint256 bondAmount) = mint(alice, amountPaid); + + // Two terms pass. A significant amount of negative interest + // accrues. + uint256 timeDelta = 2e18; + advanceTime(POSITION_DURATION.mulDown(timeDelta), -0.1e18); + + // Get some data before minting. + BurnTestCase memory testCase = _burnTestCase( + alice, // burner + bob, // destination + maturityTime, // maturity time + bondAmount, // bond amount + true, // asBase + "" // extraData + ); + + // Verify the mint transaction. + _verifyBurn(testCase, amountPaid); + } struct BurnTestCase { // Trading metadata. @@ -672,8 +350,6 @@ contract BurnTest is HyperdriveTest { /// @param _maturityTime The maturity time of the bonds to burn. /// @param _bondAmount The amount of bonds to burn. /// @param _asBase A flag indicating whether or not the deposit is in base - /// or vault shares. - /// @param _extraData The extra data for the transaction. function _burnTestCase( address _burner, address _destination, @@ -787,6 +463,9 @@ contract BurnTest is HyperdriveTest { // those paid during the burn. Burning bonds always costs twice the flat // governance fee whereas closing positions costs a combination of curve // and flat governance fees. + uint256 closeVaultSharePrice = block.timestamp < _testCase.maturityTime + ? hyperdrive.getPoolInfo().vaultSharePrice + : hyperdrive.getCheckpoint(_testCase.maturityTime).vaultSharePrice; uint256 governanceFeeAdjustment = 2 * _testCase .bondAmount @@ -795,11 +474,18 @@ contract BurnTest is HyperdriveTest { hyperdrive.calculateTimeRemaining(_testCase.maturityTime) ) .mulDown(hyperdrive.getPoolConfig().fees.governanceLP); + if (closeVaultSharePrice < openVaultSharePrice) { + governanceFeeAdjustment = governanceFeeAdjustment.mulDivDown( + closeVaultSharePrice, + openVaultSharePrice + ); + } assertApproxEqAbs( proceeds + governanceFeeAdjustment, expectedProceeds, 1e10 ); + console.log("test: 2"); // Verify that the balances increased and decreased by the right amounts. assertEq( @@ -832,7 +518,8 @@ contract BurnTest is HyperdriveTest { ); // Verify that the pool's idle increased by the flat fee minus the - // governance fees. + // governance fees. If negative interest accrued, this fee needs to be + // scaled down in proportion to the negative interest. uint256 flatFee = 2 * _testCase .bondAmount @@ -844,6 +531,12 @@ contract BurnTest is HyperdriveTest { ) ) .mulDown(ONE - hyperdrive.getPoolConfig().fees.governanceLP); + if (closeVaultSharePrice < openVaultSharePrice) { + flatFee = flatFee.mulDivDown( + closeVaultSharePrice, + openVaultSharePrice + ); + } assertApproxEqAbs( hyperdrive.idle(), _testCase.idleBefore + flatFee, @@ -870,8 +563,9 @@ contract BurnTest is HyperdriveTest { assertEq(hyperdrive.calculateSpotPrice(), _testCase.spotPriceBefore); assertEq(hyperdrive.k(), _testCase.kBefore); - // Ensure that the longs outstanding, shorts outstanding, and governance - // fees accrued decreased by the right amount. + // Ensure that the longs outstanding and shorts outstanding decreased by + // the right amount and that governance fees accrued increased by the + // right amount. If negative interest accrued, scale the governance fee. assertEq( hyperdrive.getPoolInfo().longsOutstanding, _testCase.longsOutstandingBefore - _testCase.bondAmount @@ -880,17 +574,23 @@ contract BurnTest is HyperdriveTest { hyperdrive.getPoolInfo().shortsOutstanding, _testCase.shortsOutstandingBefore - _testCase.bondAmount ); + uint256 governanceFee = 2 * + _testCase + .bondAmount + .mulUp(hyperdrive.getPoolConfig().fees.flat) + .mulDivDown( + hyperdrive.getPoolConfig().fees.governanceLP, + hyperdrive.getPoolInfo().vaultSharePrice + ); + if (closeVaultSharePrice < openVaultSharePrice) { + governanceFee = governanceFee.mulDivDown( + closeVaultSharePrice, + openVaultSharePrice + ); + } assertEq( hyperdrive.getUncollectedGovernanceFees(), - _testCase.governanceFeesAccruedBefore + - 2 * - _testCase - .bondAmount - .mulUp(hyperdrive.getPoolConfig().fees.flat) - .mulDivDown( - hyperdrive.getPoolConfig().fees.governanceLP, - hyperdrive.getPoolInfo().vaultSharePrice - ) + _testCase.governanceFeesAccruedBefore + governanceFee ); // Verify the `Burn` event. From 22a38715846b23abcab76083a74d0b1f1401dca0 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Mon, 6 Jan 2025 10:57:15 -0600 Subject: [PATCH 15/45] Fixed the remaining `burn` unit tests --- test/units/hyperdrive/BurnTest.t.sol | 49 +++++++++++++++++++++------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/test/units/hyperdrive/BurnTest.t.sol b/test/units/hyperdrive/BurnTest.t.sol index c1024cb5e..4f2d2afa5 100644 --- a/test/units/hyperdrive/BurnTest.t.sol +++ b/test/units/hyperdrive/BurnTest.t.sol @@ -238,8 +238,6 @@ contract BurnTest is HyperdriveTest { _verifyBurn(testCase, amountPaid); } - // FIXME: Figure out why this test is failing. - // /// @dev Ensure that bonds can be burned successfully halfway through the /// term after negative interest accrues. function test_burn_halfway_through_term_negative_interest() external { @@ -292,8 +290,6 @@ contract BurnTest is HyperdriveTest { _verifyBurn(testCase, amountPaid); } - // FIXME: Figure out why this test is failing. - // /// @dev Ensure that bonds can be burned successfully after maturity after /// negative interest accrues. function test_burn_after_maturity_negative_interest() external { @@ -480,12 +476,40 @@ contract BurnTest is HyperdriveTest { openVaultSharePrice ); } - assertApproxEqAbs( - proceeds + governanceFeeAdjustment, - expectedProceeds, - 1e10 - ); - console.log("test: 2"); + if ( + closeVaultSharePrice >= openVaultSharePrice || + block.timestamp >= _testCase.maturityTime + ) { + assertApproxEqAbs( + proceeds + governanceFeeAdjustment, + expectedProceeds, + 1e10 + ); + } + // If negative interest accrued and the positions are closed before + // maturity, the proceeds from burning the positions will be greater + // than those from closing the positions separately. There are a few + // reasons for this. First, the short will have a proceeds of zero since + // the short's equity was wiped out, but some of the value underlying + // the long position is still used to back the short. This means that + // the long won't get all of the value back underlying the position when + // the positions are closed separately. Second, the prepaid flat fee + // won't be returned when the positions are closed separately, but it + // will be returned when burning the positions. Even though the proceeds + // are greater than the expected proceeds, they are still less than the + // bond amount due to negative interest accruing. + // + // It might seem strange for burning to be objectively better in a + // negative interest scenario, but the improved pricing is a result of + // the system being able to properly account for all of the value + // backing the bonds. When the positions are closed separately, it can't + // track whether or not the positions are still backed without + // increasing the complexity, so it gives a worst-case performance that + // is known to be safe. + else { + assertGt(proceeds + governanceFeeAdjustment, expectedProceeds); + assertLt(proceeds, _testCase.bondAmount); + } // Verify that the balances increased and decreased by the right amounts. assertEq( @@ -588,9 +612,10 @@ contract BurnTest is HyperdriveTest { openVaultSharePrice ); } - assertEq( + assertApproxEqAbs( hyperdrive.getUncollectedGovernanceFees(), - _testCase.governanceFeesAccruedBefore + governanceFee + _testCase.governanceFeesAccruedBefore + governanceFee, + 1 ); // Verify the `Burn` event. From 39b5573b9ca1f9013dd693368023d63c730b8782 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 7 Jan 2025 09:28:09 -0600 Subject: [PATCH 16/45] Added an integration test suite for `burn` --- test/integrations/hyperdrive/BurnTest.t.sol | 294 ++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 test/integrations/hyperdrive/BurnTest.t.sol diff --git a/test/integrations/hyperdrive/BurnTest.t.sol b/test/integrations/hyperdrive/BurnTest.t.sol new file mode 100644 index 000000000..5149cb92d --- /dev/null +++ b/test/integrations/hyperdrive/BurnTest.t.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.22; + +import { VmSafe } from "forge-std/Vm.sol"; +import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; +import { AssetId } from "../../../contracts/src/libraries/AssetId.sol"; +import { FixedPointMath, ONE } from "../../../contracts/src/libraries/FixedPointMath.sol"; +import { HyperdriveMath } from "../../../contracts/src/libraries/HyperdriveMath.sol"; +import { HyperdriveTest, HyperdriveUtils } from "../../utils/HyperdriveTest.sol"; +import { Lib } from "../../utils/Lib.sol"; + +/// @dev An integration test suite for the burn function. +contract BurnTest is HyperdriveTest { + using FixedPointMath for uint256; + using HyperdriveUtils for IHyperdrive; + using Lib for *; + + /// @dev Sets up the harness and deploys and initializes a pool with fees. + function setUp() public override { + // Run the higher level setup function. + super.setUp(); + + // Deploy and initialize a pool with the flat fee and governance LP fee + // turned on. The curve fee is turned off to simplify the assertions. + IHyperdrive.PoolConfig memory config = testConfig( + 0.05e18, + POSITION_DURATION + ); + config.fees.flat = 0.0005e18; + config.fees.governanceLP = 0.15e18; + deploy(alice, config); + initialize(alice, 0.05e18, 100_000e18); + } + + /// @dev Ensures that opening and burning positions instantaneously works as + /// expected. In particular, we want to ensure that: + /// + /// - Idle increases by the flat fees and is less than or equal to the + /// base balance of Hyperdrive. + /// - The spot price remains the same. + /// - The pool depth remains the same. + /// - The trader gets the right amount of money from closing their + /// positions. + /// @param _baseAmount The amount of base to use to open long positions. + function test_open_and_burn_instantaneously(uint256 _baseAmount) external { + // Get some data before minting and closing the positions. + uint256 spotPriceBefore = hyperdrive.calculateSpotPrice(); + uint256 kBefore = hyperdrive.k(); + uint256 idleBefore = hyperdrive.idle(); + + // Alice opens some longs and shorts. + _baseAmount = _baseAmount.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + 20_000e18 + ); + (uint256 maturityTime, uint256 bondAmount) = openLong( + alice, + _baseAmount + ); + (, uint256 basePaid) = openShort(alice, bondAmount); + basePaid += _baseAmount; + + // Alice burns the positions instantaneously. + uint256 proceeds = burn(alice, maturityTime, bondAmount); + + // Ensure that Alice's total proceeds are less than the amount of base + // paid. Furthermore, we assert that the proceeds are approximately + // equal to the base amount minus the governance fees. + assertLt(proceeds, basePaid); + assertApproxEqAbs( + proceeds, + basePaid - + 2 * + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + hyperdrive.getPoolConfig().fees.governanceLP + ), + 1e6 + ); + + // Ensure that the spot price didn't change. + assertApproxEqAbs(hyperdrive.calculateSpotPrice(), spotPriceBefore, 1); + + // Ensure that the pool depth didn't change. + assertApproxEqAbs(hyperdrive.k(), kBefore, 1e6); + + // Ensure that the idle stayed roughly constant during this trade. + // Furthermore, we can assert that the pool's idle is less than or equal + // to the base balance of the Hyperdrive pool. This ensures that the + // pool thinks it is solvent and that it actually is solvent. + assertApproxEqAbs(hyperdrive.idle(), idleBefore, 1e6); + assertLe(hyperdrive.idle(), baseToken.balanceOf(address(hyperdrive))); + } + + /// @dev Ensures that opening and burning positions before maturity works as + /// expected. In particular, we want to ensure that: + /// + /// - Idle increases by the flat fees and is less than or equal to the + /// base balance of Hyperdrive. + /// - The spot price remains the same. + /// - The pool depth remains the same. + /// - The trader gets the right amount of money from closing their + /// positions. + /// @param _baseAmount The amount of base to use when opening the long + /// position. + /// @param _timeDelta The amount of time that passes. This is greater than + /// no time and less than the position duration. + /// @param _variableRate The variable rate when time passes. + function test_open_and_burn_before_maturity( + uint256 _baseAmount, + uint256 _timeDelta, + int256 _variableRate + ) external { + // Get some data before minting and closing the positions. + uint256 vaultSharePriceBefore = hyperdrive + .getPoolInfo() + .vaultSharePrice; + uint256 spotPriceBefore = hyperdrive.calculateSpotPrice(); + uint256 idleBefore = hyperdrive.idle(); + + // Alice opens some longs and shorts. + _baseAmount = _baseAmount.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + 20_000e18 + ); + (uint256 maturityTime, uint256 bondAmount) = openLong( + alice, + _baseAmount + ); + (, uint256 basePaid) = openShort(alice, bondAmount); + basePaid += _baseAmount; + + // Part of the term passes and interest accrues. + _variableRate = _variableRate.normalizeToRange(0, 2.5e18); + _timeDelta = _timeDelta.normalizeToRange( + 1, + hyperdrive.getPoolConfig().positionDuration - 1 + ); + advanceTime(_timeDelta, _variableRate); + + // Alice burns the positions before maturity. + uint256 proceeds = burn(alice, maturityTime, bondAmount); + + // Ensure that Alice's total proceeds are less than the base amount + // scaled by the amount of interest that accrued. + assertLt( + proceeds, + basePaid.mulDivDown( + hyperdrive.getPoolInfo().vaultSharePrice, + vaultSharePriceBefore + ) + ); + + // Ensure that Alice received the correct amount of proceeds from + // burning her position. She should receive the par value of the bond + // plus any interest that accrued over the term. Additionally, she + // receives the flat fee that she repaid. Then she pays the flat fee + // twice. The flat fee that she pays is scaled for the amount of time + // that passed since opening the positions. She also pays the full + // governance fee twice. + assertApproxEqAbs( + proceeds, + bondAmount.mulDivDown( + hyperdrive.getPoolInfo().vaultSharePrice, + vaultSharePriceBefore + ) + + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat) - + 2 * + bondAmount + .mulUp(hyperdrive.getPoolConfig().fees.flat) + .mulDown( + ONE - hyperdrive.calculateTimeRemaining(maturityTime) + ) + .mulDown( + ONE - hyperdrive.getPoolConfig().fees.governanceLP + ) - + 2 * + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + hyperdrive.getPoolConfig().fees.governanceLP + ), + 1e7 + ); + + // Ensure that the spot price didn't change. + assertApproxEqAbs(hyperdrive.calculateSpotPrice(), spotPriceBefore, 1); + + // Ensure that the idle only increased by the flat fee from each trade. + // Furthermore, we can assert that the pool's idle is less than or equal + // to the base balance of the Hyperdrive pool. This ensures that the + // pool thinks it is solvent and that it actually is solvent. + assertApproxEqAbs( + hyperdrive.idle(), + idleBefore.mulDivDown( + hyperdrive.getPoolInfo().vaultSharePrice, + vaultSharePriceBefore + ) + + 2 * + bondAmount + .mulUp(hyperdrive.getPoolConfig().fees.flat) + .mulDown( + ONE - hyperdrive.calculateTimeRemaining(maturityTime) + ) + .mulDown( + ONE - hyperdrive.getPoolConfig().fees.governanceLP + ), + 1e7 + ); + assertLe(hyperdrive.idle(), baseToken.balanceOf(address(hyperdrive))); + } + + /// @dev Ensures that opening and burning positions at maturity works as + /// expected. In particular, we want to ensure that: + /// + /// - Idle increases by the flat fees and is less than or equal to the + /// base balance of Hyperdrive. + /// - The spot price remains the same. + /// @param _baseAmount The amount of base to use when opening the long + /// position. + /// @param _variableRate The variable rate when time passes. + function test_open_and_burn_at_maturity( + uint256 _baseAmount, + int256 _variableRate + ) external { + // Get some data before minting and closing the positions. + uint256 vaultSharePriceBefore = hyperdrive + .getPoolInfo() + .vaultSharePrice; + uint256 spotPriceBefore = hyperdrive.calculateSpotPrice(); + uint256 idleBefore = hyperdrive.idle(); + + // Alice opens some longs and shorts. + _baseAmount = _baseAmount.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + 20_000e18 + ); + (uint256 maturityTime, uint256 bondAmount) = openLong( + alice, + _baseAmount + ); + (, uint256 basePaid) = openShort(alice, bondAmount); + basePaid += _baseAmount; + + // The term passes and interest accrues. + _variableRate = _variableRate.normalizeToRange(0, 2.5e18); + advanceTime(hyperdrive.getPoolConfig().positionDuration, _variableRate); + + // Alice burns the positions at maturity. + uint256 proceeds = burn(alice, maturityTime, bondAmount); + + // Ensure that Alice's total proceeds are less than the base amount + // scaled by the amount of interest that accrued. + assertLt( + proceeds, + basePaid.mulDivDown( + hyperdrive.getPoolInfo().vaultSharePrice, + vaultSharePriceBefore + ) + ); + + // Ensure that Alice received the correct amount of proceeds from + // closing her position. She should receive the par value of the bond + // plus any interest that accrued over the term. Additionally, she + // receives the flat fee that she repaid. Then she pays the flat fee + // twice. + assertApproxEqAbs( + proceeds, + bondAmount.mulDivDown( + hyperdrive.getPoolInfo().vaultSharePrice, + vaultSharePriceBefore + ) - bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat), + 1e6 + ); + + // Ensure that the spot price didn't change. + assertApproxEqAbs(hyperdrive.calculateSpotPrice(), spotPriceBefore, 1); + + // Ensure that the idle only increased by the flat fee from each trade. + // Furthermore, we can assert that the pool's idle is less than or equal + // to the base balance of the Hyperdrive pool. This ensures that the + // pool thinks it is solvent and that it actually is solvent. + assertApproxEqAbs( + hyperdrive.idle(), + idleBefore.mulDivDown( + hyperdrive.getPoolInfo().vaultSharePrice, + vaultSharePriceBefore + ) + + 2 * + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + ONE - hyperdrive.getPoolConfig().fees.governanceLP + ), + 1e7 + ); + assertLe(hyperdrive.idle(), baseToken.balanceOf(address(hyperdrive))); + } +} From 9399732fadcbb6df636fed39ace752092511f257 Mon Sep 17 00:00:00 2001 From: Sheng Lundquist Date: Thu, 9 Jan 2025 11:10:57 -0800 Subject: [PATCH 17/45] Bumping solidity version of mint to match rest of repo (#1235) --- contracts/src/internal/HyperdrivePair.sol | 2 +- test/integrations/hyperdrive/BurnTest.t.sol | 2 +- test/integrations/hyperdrive/MintTest.t.sol | 2 +- test/units/hyperdrive/BurnTest.t.sol | 2 +- test/units/hyperdrive/MintTest.t.sol | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index 1e92d489b..65e307445 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.22; +pragma solidity 0.8.24; import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; import { IHyperdriveEvents } from "../interfaces/IHyperdriveEvents.sol"; diff --git a/test/integrations/hyperdrive/BurnTest.t.sol b/test/integrations/hyperdrive/BurnTest.t.sol index 5149cb92d..20695e7c4 100644 --- a/test/integrations/hyperdrive/BurnTest.t.sol +++ b/test/integrations/hyperdrive/BurnTest.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.22; +pragma solidity 0.8.24; import { VmSafe } from "forge-std/Vm.sol"; import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; diff --git a/test/integrations/hyperdrive/MintTest.t.sol b/test/integrations/hyperdrive/MintTest.t.sol index 7a8308c91..c69182833 100644 --- a/test/integrations/hyperdrive/MintTest.t.sol +++ b/test/integrations/hyperdrive/MintTest.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.22; +pragma solidity 0.8.24; import { VmSafe } from "forge-std/Vm.sol"; import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; diff --git a/test/units/hyperdrive/BurnTest.t.sol b/test/units/hyperdrive/BurnTest.t.sol index 4f2d2afa5..14a0e531a 100644 --- a/test/units/hyperdrive/BurnTest.t.sol +++ b/test/units/hyperdrive/BurnTest.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.22; +pragma solidity 0.8.24; import { stdError } from "forge-std/StdError.sol"; import { VmSafe } from "forge-std/Vm.sol"; diff --git a/test/units/hyperdrive/MintTest.t.sol b/test/units/hyperdrive/MintTest.t.sol index 166a5d6d0..f36d61b46 100644 --- a/test/units/hyperdrive/MintTest.t.sol +++ b/test/units/hyperdrive/MintTest.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.22; +pragma solidity 0.8.24; import { VmSafe } from "forge-std/Vm.sol"; import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; From dd155a842f080606b6eab5106a60b1dd7b9eb90d Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 9 Jan 2025 22:29:26 -0600 Subject: [PATCH 18/45] Added tests for zombie interest for `mint` and `burn` --- contracts/src/internal/HyperdrivePair.sol | 15 +- .../hyperdrive/ZombieInterestTest.t.sol | 237 ++++++++++++++++++ 2 files changed, 241 insertions(+), 11 deletions(-) diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index 65e307445..dca3f6c49 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -5,6 +5,7 @@ import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; import { IHyperdriveEvents } from "../interfaces/IHyperdriveEvents.sol"; import { AssetId } from "../libraries/AssetId.sol"; import { FixedPointMath, ONE } from "../libraries/FixedPointMath.sol"; +import { HyperdriveMath } from "../libraries/HyperdriveMath.sol"; import { LPMath } from "../libraries/LPMath.sol"; import { SafeCast } from "../libraries/SafeCast.sol"; import { HyperdriveLP } from "./HyperdriveLP.sol"; @@ -140,8 +141,6 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { return (maturityTime, bondAmount); } - // FIXME: Document the update to the flat fee logic. - // /// @dev Burns a pair of long and short positions that directly match each /// other. The capital underlying these positions is released to the /// trader burning the positions. @@ -450,11 +449,6 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { return (bondAmount, governanceFee); } - // FIXME: Review the flat fee calculations used in this system. - // - // FIXME: Document the flat fee calculations and clearly explain why we have - // to have them here. - // /// @dev Calculates the share proceeds earned and the fees from burning the /// specified amount of bonds. /// @param _maturityTime The maturity time of the bonds to burn. @@ -557,10 +551,9 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { // NOTE: Round down to underestimate the share proceeds. uint256 bondAmount = _bondAmount; // avoid stack-too-deep uint256 vaultSharePrice = _vaultSharePrice; // avoid stack-too-deep - uint256 shareProceeds = bondAmount.mulDivDown( - closeVaultSharePrice, - vaultSharePrice.mulDown(openVaultSharePrice) - ) + + uint256 shareProceeds = bondAmount + .mulDivDown(closeVaultSharePrice, openVaultSharePrice) + .divDown(vaultSharePrice) + prepaidFlatFee - flatFee - governanceFee; diff --git a/test/integrations/hyperdrive/ZombieInterestTest.t.sol b/test/integrations/hyperdrive/ZombieInterestTest.t.sol index f04fb9ed4..f0cef5e13 100644 --- a/test/integrations/hyperdrive/ZombieInterestTest.t.sol +++ b/test/integrations/hyperdrive/ZombieInterestTest.t.sol @@ -435,6 +435,157 @@ contract ZombieInterestTest is HyperdriveTest { ); } + /// forge-config: default.fuzz.runs = 1000 + function test_zombie_interest_mint_lp( + uint256 variableRateParam, + uint256 longTradeSizeParam, + uint256 delayTimeFirstTradeParam, + uint256 zombieTimeParam, + bool removeLiquidityBeforeMaturityParam, + bool closeLongFirstParam + ) external { + _test_zombie_interest_mint_lp( + variableRateParam, + longTradeSizeParam, + delayTimeFirstTradeParam, + zombieTimeParam, + removeLiquidityBeforeMaturityParam, + closeLongFirstParam + ); + } + + /// forge-config: default.fuzz.runs = 1000 + function _test_zombie_interest_mint_lp( + uint256 variableRateParam, + uint256 mintTradeSizeParam, + uint256 delayTimeFirstTradeParam, + uint256 zombieTimeParam, + bool removeLiquidityBeforeMaturityParam, + bool burnFirstParam + ) internal { + // Initialize the pool with enough capital so that the effective share + // reserves exceed the minimum share reserves. + uint256 fixedRate = 0.035e18; + deploy(bob, fixedRate, 1e18, 0, 0, 0, 0); + initialize(bob, fixedRate, 5 * MINIMUM_SHARE_RESERVES); + + // Alice adds liquidity. + uint256 initialLiquidity = 500_000_000e18; + uint256 aliceLpShares = addLiquidity(alice, initialLiquidity); + + // Limit the fuzz testing to variableRate's less than or equal to 200%. + int256 variableRate = int256( + variableRateParam.normalizeToRange(0, 2e18) + ); + + // Ensure a feasible trade size. + uint256 mintTradeSize = mintTradeSizeParam.normalizeToRange( + 2 * MINIMUM_TRANSACTION_AMOUNT, + 500_000_000e18 + ); + + // A random amount of time passes before the long is opened. + uint256 delayTimeFirstTrade = delayTimeFirstTradeParam.normalizeToRange( + 0, + CHECKPOINT_DURATION * 10 + ); + + // A random amount of time passes after the term before the position is redeemed. + uint256 zombieTime = zombieTimeParam.normalizeToRange( + 1, + POSITION_DURATION + ); + + // Random amount of time passes before first trade. + advanceTime(delayTimeFirstTrade, variableRate); + hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive), 0); + + // Celine mints some bonds. + (uint256 maturityTime, uint256 bondsReceived) = mint( + celine, + mintTradeSize + ); + + uint256 withdrawalProceeds; + uint256 withdrawalShares; + if (removeLiquidityBeforeMaturityParam) { + // Alice removes liquidity. + (withdrawalProceeds, withdrawalShares) = removeLiquidity( + alice, + aliceLpShares + ); + } + + // One term passes and longs mature. + advanceTime(POSITION_DURATION, variableRate); + hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive), 0); + + // One term passes while we collect zombie interest. This is + // necessary to show that the zombied base amount stays constant. + uint256 zombieBaseBefore = hyperdrive + .getPoolInfo() + .zombieShareReserves + .mulDown(hyperdrive.getPoolInfo().vaultSharePrice); + advanceTimeWithCheckpoints2(POSITION_DURATION, variableRate); + uint256 zombieBaseAfter = hyperdrive + .getPoolInfo() + .zombieShareReserves + .mulDown(hyperdrive.getPoolInfo().vaultSharePrice); + assertApproxEqAbs(zombieBaseBefore, zombieBaseAfter, 1e5); + + // A random amount of time passes and interest is collected. + advanceTimeWithCheckpoints2(zombieTime, variableRate); + + uint256 proceeds; + if (burnFirstParam) { + // Celina burns late. + proceeds = burn(celine, maturityTime, bondsReceived); + if (!removeLiquidityBeforeMaturityParam) { + // Alice removes liquidity. + (withdrawalProceeds, withdrawalShares) = removeLiquidity( + alice, + aliceLpShares + ); + } + } else { + if (!removeLiquidityBeforeMaturityParam) { + // Alice removes liquidity. + (withdrawalProceeds, withdrawalShares) = removeLiquidity( + alice, + aliceLpShares + ); + } + // Celina burns late. + proceeds = burn(celine, maturityTime, bondsReceived); + } + redeemWithdrawalShares(alice, withdrawalShares); + + // Verify that the baseToken balance is within the expected range. + assertGe( + baseToken.balanceOf(address(hyperdrive)), + MINIMUM_SHARE_RESERVES + ); + + // If the share price is zero, then the hyperdrive balance is empty and there is a problem. + uint256 vaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; + assertGt(vaultSharePrice, 0); + + // Verify that the value represented in the share reserves is <= the actual amount in the contract. + uint256 baseReserves = hyperdrive.getPoolInfo().shareReserves.mulDown( + vaultSharePrice + ); + assertGe(baseToken.balanceOf(address(hyperdrive)), baseReserves); + + // Ensure that whatever is left in the zombie share reserves is <= hyperdrive contract - baseReserves. + // This is an important check bc it implies ongoing solvency. + assertLe( + hyperdrive.getPoolInfo().zombieShareReserves.mulDown( + vaultSharePrice + ), + baseToken.balanceOf(address(hyperdrive)) - baseReserves + ); + } + /// forge-config: default.fuzz.runs = 1000 function test_skipped_checkpoint( uint256 variableRateParam, @@ -795,4 +946,90 @@ contract ZombieInterestTest is HyperdriveTest { ); } } + + /// forge-config: default.fuzz.runs = 1000 + function test_zombie_mint( + uint256 mintTradeSize, + uint256 zombieTime, + bool fees + ) external { + // Initialize the pool with enough capital so that the effective share + // reserves exceed the minimum share reserves. + uint256 fixedRate = 0.05e18; + int256 variableRate = 0.05e18; + if (fees) { + deploy(bob, fixedRate, 1e18, 0.01e18, 0.0005e18, 0.15e18, 0.03e18); + } else { + deploy(bob, fixedRate, 1e18, 0, 0, 0, 0); + } + initialize(bob, fixedRate, 5 * MINIMUM_SHARE_RESERVES); + + // Alice adds liquidity. + uint256 initialLiquidity = 100_000_000e18; + addLiquidity(alice, initialLiquidity); + + // A random amount of time passes after the term before the position is redeemed. + zombieTime = zombieTime.normalizeToRange( + POSITION_DURATION, + POSITION_DURATION * 5 + ); + + // Time passes before first trade. + advanceTime(36 seconds, variableRate); + hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive), 0); + + // Celine mints some bonds. + mintTradeSize = mintTradeSize.normalizeToRange( + 2 * MINIMUM_TRANSACTION_AMOUNT, + 100_000_000e18 + ); + mint(celine, mintTradeSize); + + // One term passes and the positions mature. + advanceTimeWithCheckpoints2(POSITION_DURATION, variableRate); + + // A random amount of time passes and interest is collected. + advanceTimeWithCheckpoints2(zombieTime, variableRate); + + // Ensure that whatever is left in the zombie share reserves is + // <= hyperdrive contract - baseReserves. This is an important check bc + // it implies ongoing solvency. + uint256 vaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; + { + uint256 baseReserves = hyperdrive + .getPoolInfo() + .shareReserves + .mulDown(vaultSharePrice); + assertLe( + hyperdrive.getPoolInfo().zombieShareReserves.mulDown( + vaultSharePrice + ), + baseToken.balanceOf(address(hyperdrive)) - baseReserves + 1e9 + ); + } + + // Ensure that the lower bound for base balance is never violated (used + // in python fuzzing). + { + uint256 lowerBound = hyperdrive.getPoolInfo().shareReserves + + hyperdrive.getPoolInfo().shortsOutstanding.divDown( + vaultSharePrice + ) + + hyperdrive + .getPoolInfo() + .shortsOutstanding + .mulDown(hyperdrive.getPoolConfig().fees.flat) + .divDown(vaultSharePrice) + + hyperdrive.getUncollectedGovernanceFees() + + hyperdrive.getPoolInfo().withdrawalSharesProceeds + + hyperdrive.getPoolInfo().zombieShareReserves; + + assertLe( + lowerBound, + baseToken.balanceOf(address(hyperdrive)).divDown( + vaultSharePrice + ) + 1e9 + ); + } + } } From e6bc826bacf4f0e1d409e694dbf6b7378f6aa7dd Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 9 Jan 2025 23:30:38 -0600 Subject: [PATCH 19/45] Added more integration tests for `mint` and `burn` --- .../IntraCheckpointNettingTest.t.sol | 77 ++++++++ .../hyperdrive/LPWithdrawalTest.t.sol | 167 ++++++++++++++++++ .../hyperdrive/NonstandardDecimals.sol | 96 ++++++++++ .../hyperdrive/ReentrancyTest.t.sol | 90 +++++++++- .../hyperdrive/RoundTripTest.t.sol | 156 +++++++++++++++- .../hyperdrive/SandwichTest.t.sol | 98 ++++++++++ 6 files changed, 680 insertions(+), 4 deletions(-) diff --git a/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol b/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol index c030b53c6..a8950ac98 100644 --- a/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol +++ b/test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol @@ -155,6 +155,83 @@ contract IntraCheckpointNettingTest is HyperdriveTest { assertEq(poolInfo.shareReserves, expectedShareReserves); } + // This test was designed to show that a netted long and short created + // through minting can be burned at maturity even if all liquidity is + // removed. + function test_netting_mint_and_burn_at_maturity() external { + uint256 initialVaultSharePrice = 1e18; + int256 variableInterest = 0; + uint256 timeElapsed = 10220546; //~118 days between each trade + uint256 tradeSize = 100e18; + + // initialize the market + uint256 aliceLpShares = 0; + { + uint256 apr = 0.05e18; + deploy(alice, apr, initialVaultSharePrice, 0, 0, 0, 0); + uint256 contribution = 500_000_000e18; + aliceLpShares = initialize(alice, apr, contribution); + + // fast forward time and accrue interest + advanceTime(POSITION_DURATION, 0); + } + + // Celine adds liquidity. This is needed to allow the positions to be + // closed out. + addLiquidity(celine, 500_000_000e18); + + // open a long + uint256 basePaidLong = tradeSize; + (uint256 maturityTimeLong, uint256 bondAmountLong) = openLong( + bob, + basePaidLong + ); + + // fast forward time, create checkpoints and accrue interest + advanceTimeWithCheckpoints(timeElapsed, variableInterest); + + // mint some bonds + uint256 basePaidPair = tradeSize; + (uint256 maturityTimePair, uint256 bondAmountPair) = mint( + bob, + basePaidPair + ); + + // fast forward time, create checkpoints and accrue interest + advanceTimeWithCheckpoints(timeElapsed, variableInterest); + + // remove liquidity + (, uint256 withdrawalShares) = removeLiquidity(alice, aliceLpShares); + + // wait for the positions to mature + IHyperdrive.PoolInfo memory poolInfo = hyperdrive.getPoolInfo(); + while ( + poolInfo.shortsOutstanding > 0 || poolInfo.longsOutstanding > 0 + ) { + advanceTimeWithCheckpoints(POSITION_DURATION, variableInterest); + poolInfo = hyperdrive.getPoolInfo(); + } + + // close the long positions + closeLong(bob, maturityTimeLong, bondAmountLong); + + // close the pair positions + closeShort(bob, maturityTimePair, bondAmountPair); + + // longExposure should be 0 + poolInfo = hyperdrive.getPoolInfo(); + assertApproxEqAbs(poolInfo.longExposure, 0, 1); + + redeemWithdrawalShares(alice, withdrawalShares); + + // idle should be equal to shareReserves + uint256 expectedShareReserves = MockHyperdrive(address(hyperdrive)) + .calculateIdleShareReserves( + hyperdrive.getPoolInfo().vaultSharePrice + ) + hyperdrive.getPoolConfig().minimumShareReserves; + assertEq(poolInfo.shareReserves, expectedShareReserves); + } + function test_netting_mismatched_exposure_maturities() external { uint256 initialVaultSharePrice = 1e18; int256 variableInterest = 0e18; diff --git a/test/integrations/hyperdrive/LPWithdrawalTest.t.sol b/test/integrations/hyperdrive/LPWithdrawalTest.t.sol index c075731e8..21808872e 100644 --- a/test/integrations/hyperdrive/LPWithdrawalTest.t.sol +++ b/test/integrations/hyperdrive/LPWithdrawalTest.t.sol @@ -389,6 +389,173 @@ contract LPWithdrawalTest is HyperdriveTest { ); } + // This test is designed to ensure that an LP can remove all of their + // liquidity immediately after bonds are minted. The amount of bonds minted + // can be much larger than the total size of the pool. The LPs will receive + // capital paid at the LP share price. + function test_lp_withdrawal_pair_immediate_close( + uint256 basePaid, + int256 preTradingVariableRate + ) external { + uint256 apr = 0.05e18; + uint256 contribution = 500_000_000e18; + uint256 lpShares = initialize(alice, apr, contribution); + + // Accrue interest before the trading period. + preTradingVariableRate = preTradingVariableRate.normalizeToRange( + 0e18, + 1e18 + ); + advanceTime(POSITION_DURATION, preTradingVariableRate); + + // Bob mints some bonds. + basePaid = basePaid.normalizeToRange( + MINIMUM_TRANSACTION_AMOUNT * 2, + 10 * contribution + ); + (uint256 maturityTime, uint256 bondAmount) = mint(bob, basePaid); + assertEq(bondAmount, basePaid); + + // Alice removes all of her LP shares. The LP share price should be + // approximately equal before and after the transaction. She should + // receive base equal to the value of her LP shares and have zero + // withdrawal shares. + ( + uint256 withdrawalProceeds, + uint256 withdrawalShares + ) = removeLiquidityWithChecks(alice, lpShares); + assertApproxEqAbs( + withdrawalProceeds, + lpShares.mulDown(hyperdrive.lpSharePrice()), + 10 + ); + assertEq(withdrawalShares, 0); + + // Bob burns his bonds. He'll receive approximately the amount that he + // put in. The LP share price should be equal before and after the + // transaction. + uint256 lpSharePrice = hyperdrive.lpSharePrice(); + uint256 baseProceeds = burn(bob, maturityTime, bondAmount); + assertApproxEqAbs(baseProceeds, basePaid, 10); + assertEq(lpSharePrice, hyperdrive.lpSharePrice()); + + // Ensure that the ending base balance of Hyperdrive only consists of + // the minimum share reserves and address zero's LP shares. + assertApproxEqAbs( + baseToken.balanceOf(address(hyperdrive)), + hyperdrive.getPoolConfig().minimumShareReserves.mulDown( + hyperdrive.getPoolInfo().vaultSharePrice + + hyperdrive.lpSharePrice() + ), + 1e10 + ); + + // Ensure that no withdrawal shares are ready for withdrawal and that + // the present value of the outstanding withdrawal shares is zero. Most + // of the time, all of the withdrawal shares will be completely paid out. + // In some edge cases, the ending LP share price is small enough that + // the present value of the withdrawal shares is zero, and they won't be + // paid out. + assertEq(hyperdrive.getPoolInfo().withdrawalSharesReadyToWithdraw, 0); + assertApproxEqAbs( + hyperdrive.totalSupply(AssetId._WITHDRAWAL_SHARE_ASSET_ID).mulDown( + hyperdrive.lpSharePrice() + ), + 0, + 1 + ); + } + + // This test is designed to ensure that an LP can remove all of their + // liquidity when bonds that were minted are mature. The amount of bonds + // minted can be much larger than the total size of the pool. The LPs will + // receive capital paid at the LP share price. + function test_lp_withdrawal_pair_redemption( + uint256 basePaid, + int256 variableRate + ) external { + uint256 apr = 0.05e18; + uint256 contribution = 500_000_000e18; + uint256 lpShares = initialize(alice, apr, contribution); + contribution -= 2 * hyperdrive.getPoolConfig().minimumShareReserves; + + // Bob mints some bonds. + basePaid = basePaid.normalizeToRange( + MINIMUM_TRANSACTION_AMOUNT, + 10 * contribution + ); + (uint256 maturityTime, uint256 bondAmount) = mint(bob, basePaid); + assertEq(bondAmount, basePaid); + + // Positive interest accrues over the term. + variableRate = variableRate.normalizeToRange(0, 2e18); + advanceTime(POSITION_DURATION, variableRate); + + // Alice removes all of her LP shares. The LP share price should be + // approximately equal before and after the transaction, and the value + // of her overall portfolio should be greater than or equal to her + // original portfolio value. She should get paid exactly the amount + // implied by the LP share price and receive zero withdrawal shares. + ( + uint256 withdrawalProceeds, + uint256 withdrawalShares + ) = removeLiquidityWithChecks(alice, lpShares); + assertApproxEqAbs( + withdrawalProceeds, + lpShares.mulDown(hyperdrive.lpSharePrice()), + 1e9 + ); + assertEq(withdrawalShares, 0); + + // Bob burns his bonds. He receives the underlying value of the bonds. + // The LP share price is the same before and after. + uint256 lpSharePrice = hyperdrive.lpSharePrice(); + uint256 baseProceeds = burn(bob, maturityTime, bondAmount); + (uint256 expectedBaseProceeds, ) = HyperdriveUtils + .calculateCompoundInterest( + bondAmount, + variableRate, + POSITION_DURATION + ); + assertApproxEqAbs(baseProceeds, expectedBaseProceeds, 1e10); + assertApproxEqAbs( + lpSharePrice, + hyperdrive.lpSharePrice(), + lpSharePrice.mulDown(DISTRIBUTE_EXCESS_IDLE_ABSOLUTE_TOLERANCE) + ); + assertLe( + lpSharePrice, + hyperdrive.lpSharePrice() + + DISTRIBUTE_EXCESS_IDLE_DECREASE_TOLERANCE + ); + + // Ensure that the ending base balance of Hyperdrive only consists of + // the minimum share reserves and address zero's LP shares. + assertApproxEqAbs( + baseToken.balanceOf(address(hyperdrive)), + hyperdrive.getPoolConfig().minimumShareReserves.mulDown( + hyperdrive.getPoolInfo().vaultSharePrice + + hyperdrive.lpSharePrice() + ), + 1e10 + ); + + // Ensure that no withdrawal shares are ready for withdrawal and that + // the present value of the outstanding withdrawal shares is zero. Most + // of the time, all of the withdrawal shares will be completely paid out. + // In some edge cases, the ending LP share price is small enough that + // the present value of the withdrawal shares is zero, and they won't be + // paid out. + assertEq(hyperdrive.getPoolInfo().withdrawalSharesReadyToWithdraw, 0); + assertApproxEqAbs( + hyperdrive.totalSupply(AssetId._WITHDRAWAL_SHARE_ASSET_ID).mulDown( + hyperdrive.lpSharePrice() + ), + 0, + 1 + ); + } + struct TestLpWithdrawalParams { int256 fixedRate; int256 variableRate; diff --git a/test/integrations/hyperdrive/NonstandardDecimals.sol b/test/integrations/hyperdrive/NonstandardDecimals.sol index 50da1d3a4..845ab91cd 100644 --- a/test/integrations/hyperdrive/NonstandardDecimals.sol +++ b/test/integrations/hyperdrive/NonstandardDecimals.sol @@ -367,6 +367,102 @@ contract NonstandardDecimalsTest is HyperdriveTest { } } + function test_nonstandard_decimals_pair( + uint256 basePaid, + uint256 holdTime, + int256 variableRate + ) external { + // Normalize the fuzzed variables. + initialize(alice, 0.02e18, 500_000_000e6); + basePaid = basePaid.normalizeToRange( + hyperdrive.getPoolConfig().minimumTransactionAmount, + 1_000_000_000e6 + ); + holdTime = holdTime.normalizeToRange(0, POSITION_DURATION); + variableRate = variableRate.normalizeToRange(0, 2e18); + + // Bob mints and burns bonds immediately. He should receive essentially + // all of his capital back. + { + // Deploy and initialize the pool. + IHyperdrive.PoolConfig memory config = testConfig( + 0.02e18, + POSITION_DURATION + ); + config.minimumShareReserves = 1e6; + config.minimumTransactionAmount = 1e6; + deploy(deployer, config); + initialize(alice, 0.02e18, 500_000_000e6); + + // Bob mints some bonds. + (uint256 maturityTime, uint256 bondAmount) = mint(bob, basePaid); + + // Bob burns the bonds. + uint256 baseProceeds = burn(bob, maturityTime, bondAmount); + assertApproxEqAbs(basePaid, baseProceeds, 1e2); + } + + // Bob mints bonds and holds for a random time less than the position + // duration. He should receive the base he paid plus variable interest. + { + // Deploy and initialize the pool. + IHyperdrive.PoolConfig memory config = testConfig( + 0.02e18, + POSITION_DURATION + ); + config.minimumShareReserves = 1e6; + config.minimumTransactionAmount = 1e6; + deploy(deployer, config); + initialize(alice, 0.02e18, 500_000_000e6); + + // Bob mints some bonds. + (uint256 maturityTime, uint256 bondAmount) = mint(bob, basePaid); + + // The term passes. + advanceTime(holdTime, variableRate); + + // Bob burns the bonds. + (uint256 expectedBaseProceeds, ) = HyperdriveUtils + .calculateCompoundInterest( + basePaid, + int256(variableRate), + holdTime + ); + uint256 baseProceeds = burn(bob, maturityTime, bondAmount); + assertApproxEqAbs(baseProceeds, expectedBaseProceeds, 1e2); + } + + // Bob opens a long and holds to maturity. He should receive the face + // value of the bonds. + { + // Deploy and initialize the pool. + IHyperdrive.PoolConfig memory config = testConfig( + 0.02e18, + POSITION_DURATION + ); + config.minimumShareReserves = 1e6; + config.minimumTransactionAmount = 1e6; + deploy(deployer, config); + initialize(alice, 0.02e18, 500_000_000e6); + + // Bob mints some bonds. + (uint256 maturityTime, uint256 bondAmount) = mint(bob, basePaid); + + // The term passes. + advanceTime(POSITION_DURATION, variableRate); + + // Bob burns the bonds. + (uint256 expectedBaseProceeds, ) = HyperdriveUtils + .calculateCompoundInterest( + basePaid, + int256(variableRate), + POSITION_DURATION + ); + uint256 baseProceeds = burn(bob, maturityTime, bondAmount); + assertApproxEqAbs(baseProceeds, expectedBaseProceeds, 1e2); + } + } + struct TestLpWithdrawalParams { int256 fixedRate; int256 variableRate; diff --git a/test/integrations/hyperdrive/ReentrancyTest.t.sol b/test/integrations/hyperdrive/ReentrancyTest.t.sol index 433c2ee91..7edec387c 100644 --- a/test/integrations/hyperdrive/ReentrancyTest.t.sol +++ b/test/integrations/hyperdrive/ReentrancyTest.t.sol @@ -116,7 +116,7 @@ contract ReentrancyTest is HyperdriveTest { // Hyperdrive call. function(address, bytes memory) internal[] memory assertions = new function(address, bytes memory) internal[]( - 8 + 10 ); assertions[0] = _reenter_initialize; assertions[1] = _reenter_addLiquidity; @@ -126,6 +126,8 @@ contract ReentrancyTest is HyperdriveTest { assertions[5] = _reenter_closeLong; assertions[6] = _reenter_openShort; assertions[7] = _reenter_closeShort; + assertions[8] = _reenter_mint; + assertions[9] = _reenter_burn; // Verify that none of the core Hyperdrive functions can be reentered // with a reentrant ERC20 token. @@ -160,7 +162,7 @@ contract ReentrancyTest is HyperdriveTest { // Encode the reentrant data. We use reasonable values, but in practice, // the calls will fail immediately. With this in mind, the parameters // that are used aren't that important. - bytes[] memory data = new bytes[](9); + bytes[] memory data = new bytes[](11); data[0] = abi.encodeCall( hyperdrive.initialize, ( @@ -263,7 +265,34 @@ contract ReentrancyTest is HyperdriveTest { }) ) ); - data[8] = abi.encodeCall(hyperdrive.checkpoint, (block.timestamp, 0)); + data[8] = abi.encodeCall( + hyperdrive.mint, + ( + BOND_AMOUNT, + 0, + 0, + IHyperdrive.PairOptions({ + longDestination: _trader, + shortDestination: _trader, + asBase: true, + extraData: new bytes(0) + }) + ) + ); + data[9] = abi.encodeCall( + hyperdrive.burn, + ( + block.timestamp, + BOND_AMOUNT, + 0, + IHyperdrive.Options({ + destination: _trader, + asBase: true, + extraData: new bytes(0) + }) + ) + ); + data[10] = abi.encodeCall(hyperdrive.checkpoint, (block.timestamp, 0)); return data; } @@ -484,4 +513,59 @@ contract ReentrancyTest is HyperdriveTest { closeShort(_trader, maturityTime, BOND_AMOUNT); assert(tester.isSuccess()); } + + function _reenter_mint(address _trader, bytes memory _data) internal { + // Initialize the pool. + initialize(_trader, FIXED_RATE, CONTRIBUTION); + + // Set up the reentrant call. + tester.setTarget(address(hyperdrive)); + tester.setData(_data); + + // Ensure that `mint` can't be reentered. + mint( + _trader, + BASE_PAID, + // NOTE: Depositing more than the base payment to ensure that the + // ETH receiver will receive a refund. + DepositOverrides({ + asBase: true, + destination: _trader, + // NOTE: Roughly double deposit amount needed to cover 100% flat fee + depositAmount: BOND_AMOUNT * 2, + minSharePrice: 0, + minSlippage: 0, + maxSlippage: type(uint256).max, + extraData: new bytes(0) + }) + ); + assert(tester.isSuccess()); + } + + function _reenter_burn(address _trader, bytes memory _data) internal { + // Initialize the pool and mint some bonds. + initialize(_trader, FIXED_RATE, CONTRIBUTION); + (uint256 maturityTime, uint256 bondAmount) = mint( + _trader, + BASE_PAID, + DepositOverrides({ + asBase: true, + destination: _trader, + // NOTE: Roughly double deposit amount needed to cover 100% flat fee + depositAmount: BOND_AMOUNT * 2, + minSharePrice: 0, + minSlippage: 0, + maxSlippage: type(uint256).max, + extraData: new bytes(0) + }) + ); + + // Set up the reentrant call. + tester.setTarget(address(hyperdrive)); + tester.setData(_data); + + // Ensure that `burn` can't be reentered. + burn(_trader, maturityTime, bondAmount); + assert(tester.isSuccess()); + } } diff --git a/test/integrations/hyperdrive/RoundTripTest.t.sol b/test/integrations/hyperdrive/RoundTripTest.t.sol index 5c8634c26..de4d61975 100644 --- a/test/integrations/hyperdrive/RoundTripTest.t.sol +++ b/test/integrations/hyperdrive/RoundTripTest.t.sol @@ -56,7 +56,6 @@ contract RoundTripTest is HyperdriveTest { IHyperdrive.PoolInfo memory poolInfoAfter = hyperdrive.getPoolInfo(); // If they aren't the same, then the pool should be the one that wins. - assertGe( poolInfoAfter.shareReserves + 1e12, poolInfoBefore.shareReserves @@ -326,6 +325,161 @@ contract RoundTripTest is HyperdriveTest { ); } + /// forge-config: default.fuzz.runs = 1000 + function test_pair_round_trip_immediately_at_checkpoint( + uint256 fixedRate, + uint256 timeStretchFixedRate, + uint256 basePaid + ) external { + // Ensure a feasible fixed rate. + fixedRate = fixedRate.normalizeToRange(0.001e18, 0.50e18); + + // Ensure a feasible time stretch fixed rate. + uint256 lowerBound = fixedRate.divDown(2e18).max(0.005e18); + uint256 upperBound = lowerBound.max(fixedRate).mulDown(2e18); + timeStretchFixedRate = timeStretchFixedRate.normalizeToRange( + lowerBound, + upperBound + ); + + // Deploy the pool and initialize the market + deploy(alice, timeStretchFixedRate, 0, 0, 0, 0); + uint256 contribution = 500_000_000e18; + initialize(alice, fixedRate, contribution); + + // Ensure a feasible trade size. + basePaid = basePaid.normalizeToRange( + 2 * MINIMUM_TRANSACTION_AMOUNT, + 2 * contribution + ); + + // Get the poolInfo before opening the long. + IHyperdrive.PoolInfo memory poolInfoBefore = hyperdrive.getPoolInfo(); + + // Mint some bonds. + (uint256 maturityTime, uint256 bondAmount) = mint(bob, basePaid); + + // Immediately burn the bonds. + burn(bob, maturityTime, bondAmount); + + // Get the poolInfo after closing the long. + IHyperdrive.PoolInfo memory poolInfoAfter = hyperdrive.getPoolInfo(); + + // If they aren't the same, then the pool should be the one that wins. + assertGe( + poolInfoAfter.shareReserves + 1e12, + poolInfoBefore.shareReserves + ); + + // Should be exact if out = in. + assertEq(poolInfoAfter.bondReserves, poolInfoBefore.bondReserves); + } + + /// forge-config: default.fuzz.runs = 1000 + function test_pair_round_trip_immediately_partially_thru_checkpoint( + uint256 fixedRate, + uint256 timeStretchFixedRate, + uint256 basePaid, + uint256 timeDelta + ) external { + // Ensure a feasible fixed rate. + fixedRate = fixedRate.normalizeToRange(0.001e18, 0.50e18); + + // Ensure a feasible time stretch fixed rate. + uint256 lowerBound = fixedRate.divDown(2e18).max(0.005e18); + uint256 upperBound = lowerBound.max(fixedRate).mulDown(2e18); + timeStretchFixedRate = timeStretchFixedRate.normalizeToRange( + lowerBound, + upperBound + ); + + // Deploy the pool and initialize the market + deploy(alice, timeStretchFixedRate, 0, 0, 0, 0); + uint256 contribution = 500_000_000e18; + initialize(alice, fixedRate, contribution); + + // Ensure a feasible trade size. + basePaid = basePaid.normalizeToRange( + 2 * MINIMUM_TRANSACTION_AMOUNT, + 2 * contribution + ); + + // Calculate time elapsed. + timeDelta = timeDelta.normalizeToRange(0, CHECKPOINT_DURATION - 1); + + // Fast forward time. + advanceTime(timeDelta, 0); + + // Get the poolInfo before opening the long. + IHyperdrive.PoolInfo memory poolInfoBefore = hyperdrive.getPoolInfo(); + + // Mint some bonds. + (uint256 maturityTime, uint256 bondAmount) = mint(bob, basePaid); + + // Immediately burn the bonds. + burn(bob, maturityTime, bondAmount); + + // Get the poolInfo after closing the long. + IHyperdrive.PoolInfo memory poolInfoAfter = hyperdrive.getPoolInfo(); + + // If they aren't the same, then the pool should be the one that wins. + assertGe( + poolInfoAfter.shareReserves + 1e12, + poolInfoBefore.shareReserves + ); + + // Should be exact if out = in. + assertEq(poolInfoAfter.bondReserves, poolInfoBefore.bondReserves); + } + + /// forge-config: default.fuzz.runs = 1000 + function test_pair_round_trip_immediately_with_fees( + uint256 fixedRate, + uint256 timeStretchFixedRate, + uint256 basePaid + ) external { + // Ensure a feasible fixed rate. + fixedRate = fixedRate.normalizeToRange(0.001e18, 0.50e18); + + // Ensure a feasible time stretch fixed rate. + uint256 lowerBound = fixedRate.divDown(2e18).max(0.005e18); + uint256 upperBound = lowerBound.max(fixedRate).mulDown(2e18); + timeStretchFixedRate = timeStretchFixedRate.normalizeToRange( + lowerBound, + upperBound + ); + + // Deploy the pool and initialize the market + IHyperdrive.PoolConfig memory config = testConfig( + timeStretchFixedRate, + POSITION_DURATION + ); + config.fees.curve = 0.1e18; + config.fees.governanceLP = 1e18; + deploy(alice, config); + uint256 contribution = 500_000_000e18; + initialize(alice, fixedRate, contribution); + + // Ensure a feasible trade size. + basePaid = basePaid.normalizeToRange( + 2 * MINIMUM_TRANSACTION_AMOUNT, + 2 * contribution + ); + + // Bob mints some bonds. + (uint256 maturityTime, uint256 bondAmount) = mint(bob, basePaid); + + // Bob immediately burns the bonds. + IHyperdrive.PoolInfo memory infoBefore = hyperdrive.getPoolInfo(); + burn(bob, maturityTime, bondAmount); + + // Ensure that the share adjustment wasn't changed. + assertEq( + hyperdrive.getPoolInfo().shareAdjustment, + infoBefore.shareAdjustment + ); + } + /// forge-config: default.fuzz.runs = 1000 function test_sandwiched_long_round_trip( uint256 fixedRate, diff --git a/test/integrations/hyperdrive/SandwichTest.t.sol b/test/integrations/hyperdrive/SandwichTest.t.sol index 1efe0d6b3..f44ace5c8 100644 --- a/test/integrations/hyperdrive/SandwichTest.t.sol +++ b/test/integrations/hyperdrive/SandwichTest.t.sol @@ -231,6 +231,104 @@ contract SandwichTest is HyperdriveTest { assertGe(withdrawalProceeds, contribution); } + function test_sandwich_pair_with_long( + uint256 apr, + uint256 tradeSize + ) external { + // limit the fuzz testing to variableRate's less than or equal to 50% + apr = apr.normalizeToRange(0.01e18, 0.2e18); + + // ensure a feasible trade size + tradeSize = tradeSize.normalizeToRange(1_000e18, 10_000_000e18); + + // Deploy the pool and initialize the market + { + uint256 timeStretchApr = 0.05e18; + deploy(alice, timeStretchApr, 0, 0, 0, 0); + } + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Calculate the amount of bonds minted during a sandwich. + uint256 bondsReceived; + { + // open a long. + uint256 longBasePaid = tradeSize; // 10_000_000e18; + openLong(bob, longBasePaid); + + // mint some bonds. + uint256 basePaidPair = tradeSize; //10_000_000e18; + (, bondsReceived) = mint(bob, basePaidPair); + } + + // Deploy the pool and initialize the market + { + uint256 timeStretchApr = 0.05e18; + deploy(alice, timeStretchApr, 0, 0, 0, 0); + } + initialize(alice, apr, contribution); + + // Calculate how many bonds would be minted without the sandwich. + uint256 baselineBondsReceived; + { + // mint some bonds. + uint256 basePaidPair = tradeSize; //10_000_000e18; + (, baselineBondsReceived) = mint(bob, basePaidPair); + } + + // Ensure that the bonds minted are the same in either case. + assertEq(baselineBondsReceived, bondsReceived); + } + + function test_sandwich_pair_with_short( + uint256 apr, + uint256 tradeSize + ) external { + // limit the fuzz testing to variableRate's less than or equal to 50% + apr = apr.normalizeToRange(0.01e18, 0.2e18); + + // ensure a feasible trade size + tradeSize = tradeSize.normalizeToRange(1_000e18, 10_000_000e18); + + // Deploy the pool and initialize the market + { + uint256 timeStretchApr = 0.05e18; + deploy(alice, timeStretchApr, 0, 0, 0, 0); + } + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Calculate the amount of bonds minted during a sandwich. + uint256 bondsReceived; + { + // open a short. + uint256 shortAmount = tradeSize; // 10_000_000e18; + openShort(bob, shortAmount); + + // mint some bonds. + uint256 basePaidPair = tradeSize; //10_000_000e18; + (, bondsReceived) = mint(bob, basePaidPair); + } + + // Deploy the pool and initialize the market + { + uint256 timeStretchApr = 0.05e18; + deploy(alice, timeStretchApr, 0, 0, 0, 0); + } + initialize(alice, apr, contribution); + + // Calculate how many bonds would be minted without the sandwich. + uint256 baselineBondsReceived; + { + // mint some bonds. + uint256 basePaidPair = tradeSize; //10_000_000e18; + (, baselineBondsReceived) = mint(bob, basePaidPair); + } + + // Ensure that the bonds minted are the same in either case. + assertEq(baselineBondsReceived, bondsReceived); + } + function test_sandwich_lp(uint256 apr) external { apr = apr.normalizeToRange(0.01e18, 0.2e18); From 1203d123b2f90f6d65a91a5c00e608cca7b604b5 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 9 Jan 2025 23:36:36 -0600 Subject: [PATCH 20/45] Added a negative interest test for `mint` --- test/units/hyperdrive/MintTest.t.sol | 39 +++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/test/units/hyperdrive/MintTest.t.sol b/test/units/hyperdrive/MintTest.t.sol index f36d61b46..0835f157b 100644 --- a/test/units/hyperdrive/MintTest.t.sol +++ b/test/units/hyperdrive/MintTest.t.sol @@ -210,7 +210,8 @@ contract MintTest is HyperdriveTest { _verifyMint(testCase); } - /// @dev Ensures that minting performs correctly when it succeeds. + /// @dev Ensures that minting performs correctly when there is prepaid + /// interest. function test_mint_success_prepaid_interest() external { // Mint some base tokens to Alice and approve Hyperdrive. vm.stopPrank(); @@ -238,6 +239,35 @@ contract MintTest is HyperdriveTest { _verifyMint(testCase); } + /// @dev Ensures that minting performs correctly when negative interest + /// accrues. + function test_mint_success_negative_interest() external { + // Mint some base tokens to Alice and approve Hyperdrive. + vm.stopPrank(); + vm.startPrank(alice); + uint256 baseAmount = 100_000e18; + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + + // Mint a checkpoint and accrue interest. This sets us up to have + // prepaid interest to account for. + hyperdrive.checkpoint(hyperdrive.latestCheckpoint(), 0); + advanceTime(CHECKPOINT_DURATION.mulDown(0.5e18), -0.2e18); + + // Get some data before minting. + MintTestCase memory testCase = _mintTestCase( + alice, // funder + bob, // long + celine, // short + baseAmount, // amount + true, // asBase + "" // extraData + ); + + // Verify the mint transaction. + _verifyMint(testCase); + } + struct MintTestCase { // Trading metadata. address funder; @@ -416,7 +446,10 @@ contract MintTest is HyperdriveTest { .vaultSharePrice; uint256 requiredBaseAmount = bondAmount + bondAmount.mulDivDown( - hyperdrive.getPoolInfo().vaultSharePrice - openVaultSharePrice, + hyperdrive.getPoolInfo().vaultSharePrice - + openVaultSharePrice.min( + hyperdrive.getPoolInfo().vaultSharePrice + ), openVaultSharePrice ) + bondAmount.mulDown(hyperdrive.getPoolConfig().fees.flat) + @@ -425,7 +458,7 @@ contract MintTest is HyperdriveTest { hyperdrive.getPoolConfig().fees.governanceLP ); assertGt(_testCase.amount, requiredBaseAmount); - assertApproxEqAbs(_testCase.amount, requiredBaseAmount, 10); + assertApproxEqAbs(_testCase.amount, requiredBaseAmount, 1e6); // Verify the `Mint` event. _verifyMintEvent(_testCase, bondAmount); From f96ea1fca1b0d981b74072657458e2e31b3d2ff4 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 9 Jan 2025 23:42:22 -0600 Subject: [PATCH 21/45] Addressed review feedback from @Sean329 --- contracts/src/internal/HyperdrivePair.sol | 15 ++++++++------- python/gas_benchmarks.py | 2 ++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index dca3f6c49..14f9737cc 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -414,7 +414,7 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { // This implies that // // bondAmount = shareDeposited * vaultSharePrice / ( - // 1 + (c - c0) / c0 + flatFee + 2 * flatFee * governanceFee + // 1 + (max(c, c0) - c0) / c0 + flatFee + 2 * flatFee * governanceFee // ) // // NOTE: We round down to underestimate the bond amount. @@ -422,12 +422,13 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { _vaultSharePrice, // NOTE: Round up to overestimate the denominator. This // underestimates the bond amount. - (ONE + - // NOTE: If negative interest has accrued and the open vault - // share price is greater than the vault share price, we clamp - // the vault share price to the open vault share price. - (_vaultSharePrice.max(_openVaultSharePrice) - - _openVaultSharePrice).divUp(_openVaultSharePrice) + + // + // NOTE: If negative interest has accrued and the open vault share + // price is greater than the vault share price, we clamp the vault + // share price to the open vault share price. + ((_vaultSharePrice.max(_openVaultSharePrice)).divUp( + _openVaultSharePrice + ) + _flatFee + 2 * _flatFee.mulUp(_governanceLPFee)) diff --git a/python/gas_benchmarks.py b/python/gas_benchmarks.py index 14bc1f1aa..caf46fac4 100644 --- a/python/gas_benchmarks.py +++ b/python/gas_benchmarks.py @@ -14,6 +14,8 @@ "closeLong", "openShort", "closeShort", + "mint", + "burn", "checkpoint", ] From bcc9c16a8c618a08e45dcd5344f2aae32d8741a9 Mon Sep 17 00:00:00 2001 From: Sheng Lundquist Date: Thu, 9 Jan 2025 10:26:26 -0800 Subject: [PATCH 22/45] Adding script for mint fuzz --- python-fuzz/fuzz_mint_burn.py | 316 ++++++++++++++++++++++++++++++++++ python-fuzz/requirements.txt | 2 + 2 files changed, 318 insertions(+) create mode 100644 python-fuzz/fuzz_mint_burn.py create mode 100644 python-fuzz/requirements.txt diff --git a/python-fuzz/fuzz_mint_burn.py b/python-fuzz/fuzz_mint_burn.py new file mode 100644 index 000000000..08231c2a3 --- /dev/null +++ b/python-fuzz/fuzz_mint_burn.py @@ -0,0 +1,316 @@ +"""Bots for fuzzing hyperdrive, along with mint/burn. +""" + +from __future__ import annotations + +import argparse +import logging +import os +import random +import sys +from typing import NamedTuple, Sequence + +import numpy as np +from agent0 import LocalChain, LocalHyperdrive +from agent0.hyperfuzz.system_fuzz import generate_fuzz_hyperdrive_config, run_fuzz_bots +from agent0.hyperlogs.rollbar_utilities import initialize_rollbar, log_rollbar_exception +from eth_account.account import Account +from eth_account.signers.local import LocalAccount +from fixedpointmath import FixedPoint +from hyperdrivetypes.types.IHyperdrive import PairOptions +from pypechain.core import PypechainCallException +from web3 import Web3 +from web3.exceptions import ContractCustomError + + +def _fuzz_ignore_logging_to_rollbar(exc: Exception) -> bool: + """Function defining errors to not log to rollbar during fuzzing. + + These are the two most common errors we see in local fuzz testing. These are + known issues due to random bots not accounting for these cases, so we don't log them to + rollbar. + """ + if isinstance(exc, PypechainCallException): + orig_exception = exc.orig_exception + if orig_exception is None: + return False + + # Insufficient liquidity error + if isinstance(orig_exception, ContractCustomError) and exc.decoded_error == "InsufficientLiquidity()": + return True + + # Circuit breaker triggered error + if isinstance(orig_exception, ContractCustomError) and exc.decoded_error == "CircuitBreakerTriggered()": + return True + + return False + + +def _fuzz_ignore_errors(exc: Exception) -> bool: + """Function defining errors to ignore during fuzzing of hyperdrive pools.""" + # pylint: disable=too-many-return-statements + # pylint: disable=too-many-branches + # Ignored fuzz exceptions + + # Contract call exceptions + if isinstance(exc, PypechainCallException): + orig_exception = exc.orig_exception + if orig_exception is None: + return False + + # Insufficient liquidity error + if isinstance(orig_exception, ContractCustomError) and exc.decoded_error == "InsufficientLiquidity()": + return True + + # Circuit breaker triggered error + if isinstance(orig_exception, ContractCustomError) and exc.decoded_error == "CircuitBreakerTriggered()": + return True + + # DistributeExcessIdle error + if isinstance(orig_exception, ContractCustomError) and exc.decoded_error == "DistributeExcessIdleFailed()": + return True + + # MinimumTransactionAmount error + if isinstance(orig_exception, ContractCustomError) and exc.decoded_error == "MinimumTransactionAmount()": + return True + + # DecreasedPresentValueWhenAddingLiquidity error + if ( + isinstance(orig_exception, ContractCustomError) + and exc.decoded_error == "DecreasedPresentValueWhenAddingLiquidity()" + ): + return True + + # Closing long results in fees exceeding long proceeds + if len(exc.args) > 1 and "Closing the long results in fees exceeding long proceeds" in exc.args[0]: + return True + + # # Status == 0 + # if ( + # isinstance(orig_exception, FailedTransaction) + # and len(orig_exception.args) > 0 + # and "Receipt has status of 0" in orig_exception.args[0] + # ): + # return True + + return False + + +def main(argv: Sequence[str] | None = None) -> None: + """Runs the mint/burn fuzzing. + + Arguments + --------- + argv: Sequence[str] + The argv values returned from argparser. + """ + # pylint: disable=too-many-branches + + parsed_args = parse_arguments(argv) + + # Negative rng_seed means default + if parsed_args.rng_seed < 0: + rng_seed = random.randint(0, 10000000) + else: + rng_seed = parsed_args.rng_seed + rng = np.random.default_rng(rng_seed) + + # Set up rollbar + # TODO log additional crashes + rollbar_environment_name = "fuzz_mint_burn" + log_to_rollbar = initialize_rollbar(rollbar_environment_name) + + # Set up chain config + local_chain_config = LocalChain.Config( + block_timestamp_interval=12, + log_level_threshold=logging.WARNING, + preview_before_trade=True, + log_to_rollbar=log_to_rollbar, + rollbar_log_prefix="localfuzzbots", + rollbar_log_filter_func=_fuzz_ignore_logging_to_rollbar, + rng=rng, + crash_log_level=logging.ERROR, + rollbar_log_level_threshold=logging.ERROR, # Only log errors and above to rollbar + crash_report_additional_info={"rng_seed": rng_seed}, + gas_limit=int(1e6), # Plenty of gas limit for transactions + ) + + while True: + # Build interactive local hyperdrive + # TODO can likely reuse some of these resources + # instead, we start from scratch every time. + chain = LocalChain(local_chain_config) + + # Fuzz over config values + hyperdrive_config = generate_fuzz_hyperdrive_config(rng, lp_share_price_test=False, steth=False) + + try: + hyperdrive_pool = LocalHyperdrive(chain, hyperdrive_config) + except Exception as e: # pylint: disable=broad-except + logging.error( + "Error deploying hyperdrive: %s", + repr(e), + ) + log_rollbar_exception( + e, + log_level=logging.ERROR, + rollbar_log_prefix="Error deploying hyperdrive poolError deploying hyperdrive pool", + ) + chain.cleanup() + continue + + agents = None + + # Run the fuzzing bot for an episode + for _ in range(parsed_args.num_iterations_per_episode): + # Run fuzzing via agent0 function on underlying hyperdrive pool. + # By default, this sets up 4 agents. + # `check_invariance` also runs the pool's invariance checks after trades. + # We only run for 1 iteration here, as we want to make additional random trades + # wrt mint/burn. + agents = run_fuzz_bots( + chain, + hyperdrive_pools=[hyperdrive_pool], + # We pass in the same agents when running fuzzing + agents=agents, + check_invariance=True, + raise_error_on_failed_invariance_checks=True, + raise_error_on_crash=True, + log_to_rollbar=log_to_rollbar, + ignore_raise_error_func=_fuzz_ignore_errors, + random_advance_time=False, # We take care of advancing time in the outer loop + lp_share_price_test=False, + base_budget_per_bot=FixedPoint(1_000_000), + num_iterations=1, + minimum_avg_agent_base=FixedPoint(100_000), + ) + + # Get access to the underlying hyperdrive contract for pypechain calls + hyperdrive_contract = hyperdrive_pool.interface.hyperdrive_contract + + # Run random vault mint/burn + for agent in agents: + # Pick mint or burn at random + trade = chain.config.rng.choice(["mint", "burn"]) # type: ignore + match trade: + case "mint": + balance = agent.get_wallet().balance.amount + if balance > 0: + # TODO can't use numpy rng since it doesn't support uint256. + # Need to use the state from the chain config to use the same rng object. + amount = random.randint(0, balance.scaled_value) + logging.info(f"Agent {agent.address} is calling minting with {amount}") + + # FIXME figure out what these options are + pair_options = PairOptions( + longDestination=agent.address, + shortDestination=agent.address, + asBase=True, + extraData=bytes(0), + ) + + hyperdrive_contract.functions.mint( + _amount=amount, _minOutput=0, _minVaultSharePrice=0, _options=pair_options + ).sign_transact_and_wait(account=agent.account, validate_transaction=True) + + case "burn": + # FIXME figure out in what cases an agent can burn tokens + agent_longs = agent.get_longs() + num_longs = len(agent_longs) + if num_longs > 0 and agent_longs[0].balance > 0: + amount = random.randint(0, balance.scaled_value) + logging.info(f"Agent {agent.address} is calling burn with {amount}") + + # FIXME figure out what these options are + pair_options = PairOptions( + longDestination=agent.address, + shortDestination=agent.address, + asBase=True, + extraData=bytes(0), + ) + + # FIXME figure out what _maturityTime is + # FIXME burn is expecting `Options`, not `PairOptions` + hyperdrive_contract.functions.burn( + _maturityTime=0, _bondAmount=0, _minOutput=0, _options=pair_options + ).sign_transact_and_wait(account=agent.account, validate_transaction=True) + + # Advance time for a day + # TODO parameterize the amount of time to advance. + chain.advance_time(60 * 60 * 24) + + +class Args(NamedTuple): + """Command line arguments for fuzzing mint/burn.""" + + rng_seed: int + num_iterations_per_episode: int + + +def namespace_to_args(namespace: argparse.Namespace) -> Args: + """Converts argprase.Namespace to Args. + + Arguments + --------- + namespace: argparse.Namespace + Object for storing arg attributes. + + Returns + ------- + Args + Formatted arguments + """ + return Args( + rng_seed=namespace.rng_seed, + num_iterations_per_episode=namespace.num_iterations_per_episode, + ) + + +def parse_arguments(argv: Sequence[str] | None = None) -> Args: + """Parses input arguments. + + Arguments + --------- + argv: Sequence[str] + The argv values returned from argparser. + + Returns + ------- + Args + Formatted arguments + """ + parser = argparse.ArgumentParser(description="Runs fuzzing mint/burn") + + parser.add_argument( + "--rng-seed", + type=int, + default=-1, + help="The random seed to use for the fuzz run.", + ) + parser.add_argument( + "--num-iterations-per-episode", + default=3000, + help="The number of iterations to run for each random pool config.", + ) + + # Use system arguments if none were passed + if argv is None: + argv = sys.argv + return namespace_to_args(parser.parse_args()) + + +# Run fuzing +if __name__ == "__main__": + # Wrap everything in a try catch to log any non-caught critical errors and log to rollbar + try: + main() + except BaseException as exc: # pylint: disable=broad-except + # pylint: disable=invalid-name + _rpc_uri = os.getenv("RPC_URI", None) + if _rpc_uri is None: + _log_prefix = "Uncaught Critical Error in Fuzzing mint/burn:" + else: + _chain_name = _rpc_uri.split("//")[-1].split("/")[0] + _log_prefix = f"Uncaught Critical Error for {_chain_name} in Fuzz mint/burn:" + log_rollbar_exception(exception=exc, log_level=logging.CRITICAL, rollbar_log_prefix=_log_prefix) + raise exc diff --git a/python-fuzz/requirements.txt b/python-fuzz/requirements.txt new file mode 100644 index 000000000..12763d2b6 --- /dev/null +++ b/python-fuzz/requirements.txt @@ -0,0 +1,2 @@ +-e python/hyperdrivetypes +-e ../agent0 \ No newline at end of file From 638df4bf8379cbeb6c2de7afcecc2eda7acab62f Mon Sep 17 00:00:00 2001 From: Sheng Lundquist Date: Thu, 9 Jan 2025 10:51:42 -0800 Subject: [PATCH 23/45] Adding git ignore for agent0 output --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e783d49e5..2ca567ee5 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ etherscan_requests/ # todos TODO.md + +# Agent0 fuzzing output files +.crash_report \ No newline at end of file From 23b7978c2ed991223dafe6e72444c383a1808153 Mon Sep 17 00:00:00 2001 From: Sheng Lundquist Date: Thu, 9 Jan 2025 10:52:12 -0800 Subject: [PATCH 24/45] Removing unused imports and using option field in burn --- python-fuzz/fuzz_mint_burn.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/python-fuzz/fuzz_mint_burn.py b/python-fuzz/fuzz_mint_burn.py index 08231c2a3..a9834ed5d 100644 --- a/python-fuzz/fuzz_mint_burn.py +++ b/python-fuzz/fuzz_mint_burn.py @@ -14,12 +14,9 @@ from agent0 import LocalChain, LocalHyperdrive from agent0.hyperfuzz.system_fuzz import generate_fuzz_hyperdrive_config, run_fuzz_bots from agent0.hyperlogs.rollbar_utilities import initialize_rollbar, log_rollbar_exception -from eth_account.account import Account -from eth_account.signers.local import LocalAccount from fixedpointmath import FixedPoint -from hyperdrivetypes.types.IHyperdrive import PairOptions +from hyperdrivetypes.types.IHyperdrive import Options, PairOptions from pypechain.core import PypechainCallException -from web3 import Web3 from web3.exceptions import ContractCustomError @@ -222,9 +219,14 @@ def main(argv: Sequence[str] | None = None) -> None: logging.info(f"Agent {agent.address} is calling burn with {amount}") # FIXME figure out what these options are - pair_options = PairOptions( - longDestination=agent.address, - shortDestination=agent.address, + # pair_options = PairOptions( + # longDestination=agent.address, + # shortDestination=agent.address, + # asBase=True, + # extraData=bytes(0), + # ) + options = Options( + destination=agent.address, asBase=True, extraData=bytes(0), ) @@ -232,7 +234,7 @@ def main(argv: Sequence[str] | None = None) -> None: # FIXME figure out what _maturityTime is # FIXME burn is expecting `Options`, not `PairOptions` hyperdrive_contract.functions.burn( - _maturityTime=0, _bondAmount=0, _minOutput=0, _options=pair_options + _maturityTime=0, _bondAmount=0, _minOutput=0, _options=options ).sign_transact_and_wait(account=agent.account, validate_transaction=True) # Advance time for a day From 54f7669fc09e4750fb8d30027b3b7f2362813e7c Mon Sep 17 00:00:00 2001 From: Sheng Lundquist Date: Thu, 9 Jan 2025 11:20:23 -0800 Subject: [PATCH 25/45] Adding fixme comment --- python-fuzz/fuzz_mint_burn.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python-fuzz/fuzz_mint_burn.py b/python-fuzz/fuzz_mint_burn.py index a9834ed5d..bad1a64a6 100644 --- a/python-fuzz/fuzz_mint_burn.py +++ b/python-fuzz/fuzz_mint_burn.py @@ -237,6 +237,8 @@ def main(argv: Sequence[str] | None = None) -> None: _maturityTime=0, _bondAmount=0, _minOutput=0, _options=options ).sign_transact_and_wait(account=agent.account, validate_transaction=True) + # FIXME add any additional invariance checks specific to mint/burn here. + # Advance time for a day # TODO parameterize the amount of time to advance. chain.advance_time(60 * 60 * 24) From 0b1df87b771474f07f4bd3cfe7ee92a081f630dc Mon Sep 17 00:00:00 2001 From: Sheng Lundquist Date: Thu, 9 Jan 2025 11:28:44 -0800 Subject: [PATCH 26/45] Another fixme comment --- python-fuzz/fuzz_mint_burn.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python-fuzz/fuzz_mint_burn.py b/python-fuzz/fuzz_mint_burn.py index bad1a64a6..ea14f430e 100644 --- a/python-fuzz/fuzz_mint_burn.py +++ b/python-fuzz/fuzz_mint_burn.py @@ -132,6 +132,9 @@ def main(argv: Sequence[str] | None = None) -> None: gas_limit=int(1e6), # Plenty of gas limit for transactions ) + # FIXME wrap all of this in a try catch to catch any exceptions thrown in fuzzing. + # When an error occurs, we likely want to pause the chain to allow for remote connection + # for debugging while True: # Build interactive local hyperdrive # TODO can likely reuse some of these resources From 1420bfa64f60c6d30a1cc0d3fb1f66af2c13c2b9 Mon Sep 17 00:00:00 2001 From: Sheng Lundquist Date: Thu, 9 Jan 2025 11:29:03 -0800 Subject: [PATCH 27/45] Adding readme to python fuzzing --- python-fuzz/README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 python-fuzz/README.md diff --git a/python-fuzz/README.md b/python-fuzz/README.md new file mode 100644 index 000000000..d8fa73bd9 --- /dev/null +++ b/python-fuzz/README.md @@ -0,0 +1,35 @@ +# Python fuzzing for mint/burn + +This directory details how to install and run fuzzing on hyperdrive with mint/burn. + +## Installation + +First, compile the solidity contracts and make python types locally via `make`. + +Next, follow the prerequisites installation instructions of [agent0](https://github.com/delvtech/agent0/blob/main/INSTALL.md). +Then install [uv](https://github.com/astral-sh/uv) for package management. No need to clone the repo locally +(unless developing on agent0). + +From the base directory of the `hyperdrive` repo, set up a python virtual environment: + +``` +uv venv --python 3.10 .venv +source .venv/bin/activate +``` + +From here, you can install the generated python types and agent0 via: + +``` +uv pip install -r python-fuzz/requirements.txt +``` + +## Running fuzzing + +To run fuzzing, simply run the `fuzz_mint_burn.py` script: + +``` +python fuzz_mint_burn.py +``` + + + From 76af95515093b7932359a94a30a67ae666a0a699 Mon Sep 17 00:00:00 2001 From: Sheng Lundquist Date: Thu, 9 Jan 2025 11:44:29 -0800 Subject: [PATCH 28/45] Pointing agent0 req to pypi --- python-fuzz/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-fuzz/requirements.txt b/python-fuzz/requirements.txt index 12763d2b6..e803a1d22 100644 --- a/python-fuzz/requirements.txt +++ b/python-fuzz/requirements.txt @@ -1,2 +1,2 @@ -e python/hyperdrivetypes --e ../agent0 \ No newline at end of file +agent0 >= 0.26.2 \ No newline at end of file From 1fecbc1a1874ebe670eacc9564313728a1c1181f Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 10 Jan 2025 15:17:41 -0600 Subject: [PATCH 29/45] Committed incremental progress --- contracts/src/internal/HyperdrivePair.sol | 4 +- test/instances/ezETH/EzETHHyperdrive.t.sol | 2 +- test/utils/InstanceTest.sol | 884 +++++++++++++++++++++ 3 files changed, 887 insertions(+), 3 deletions(-) diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index 14f9737cc..bbe98ccde 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -411,10 +411,10 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { // 2 * bondAmount * flatFee * governanceFee // ) // - // This implies that + // This implies that: // // bondAmount = shareDeposited * vaultSharePrice / ( - // 1 + (max(c, c0) - c0) / c0 + flatFee + 2 * flatFee * governanceFee + // max(c, c0) / c0 + flatFee + 2 * flatFee * governanceFee // ) // // NOTE: We round down to underestimate the bond amount. diff --git a/test/instances/ezETH/EzETHHyperdrive.t.sol b/test/instances/ezETH/EzETHHyperdrive.t.sol index 57e7ca2eb..5490c3ed7 100644 --- a/test/instances/ezETH/EzETHHyperdrive.t.sol +++ b/test/instances/ezETH/EzETHHyperdrive.t.sol @@ -111,7 +111,7 @@ contract EzETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesTolerance: 1e7, roundTripShortMaturityWithSharesTolerance: 1e8, // The verification tolerances. - verifyDepositTolerance: 2, + verifyDepositTolerance: 300, verifyWithdrawalTolerance: 1_000 }) ) diff --git a/test/utils/InstanceTest.sol b/test/utils/InstanceTest.sol index 649ad011c..726e3e648 100644 --- a/test/utils/InstanceTest.sol +++ b/test/utils/InstanceTest.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; +// FIXME +import { console2 as console } from "forge-std/console2.sol"; + import { ERC20ForwarderFactory } from "../../contracts/src/token/ERC20ForwarderFactory.sol"; import { HyperdriveFactory } from "../../contracts/src/factory/HyperdriveFactory.sol"; import { IERC20 } from "../../contracts/src/interfaces/IERC20.sol"; @@ -3060,6 +3063,887 @@ abstract contract InstanceTest is HyperdriveTest { ); } + /// Pair /// + + /// @dev A test to make sure that ETH is handled correctly when bonds are + /// minted. Instances that accept ETH should give users refunds when + /// they submit too much ETH, and instances that don't accept ETH + /// should revert. + function test_mint_with_eth() external { + vm.startPrank(bob); + + if (isBaseETH && config.enableBaseDeposits) { + // Ensure that Bob receives a refund on the excess ETH that he sent + // when minting bonds with "asBase" set to true. + uint256 ethBalanceBefore = address(bob).balance; + hyperdrive.mint{ value: 2e18 }( + 1e18, + 0, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + assertEq(address(bob).balance, ethBalanceBefore - 1e18); + + // Ensure that Bob receives a refund when he mints a bond with + // "asBase" set to false and sends ether to the contract. + ethBalanceBefore = address(bob).balance; + hyperdrive.mint{ value: 0.5e18 }( + 1e18, + 0, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: false, + extraData: new bytes(0) + }) + ); + assertEq(address(bob).balance, ethBalanceBefore); + } else { + // Ensure that sending ETH to `mint` fails with `asBase` as true. + vm.expectRevert(IHyperdrive.NotPayable.selector); + hyperdrive.mint{ value: 2e18 }( + 1e18, + 0, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + + // Ensure that sending ETH to `mint` fails with `asBase` as false. + vm.expectRevert(IHyperdrive.NotPayable.selector); + hyperdrive.mint{ value: 0.5e18 }( + 1e18, + 0, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: false, + extraData: new bytes(0) + }) + ); + } + } + + // FIXME: Make some of these updates to the other instance tests. + // + /// @dev Fuzz test to ensure deposit accounting is correct when mint bonds + /// with the share token. This test case is expected to fail if share + /// deposits are not supported. + /// @param sharesPaid Amount in terms of shares to mint the bonds. + function test_mint_with_shares(uint256 sharesPaid) external { + // Early termination if share deposits are not supported. + if (!config.enableShareDeposits) { + return; + } + + // Get balance information before opening a long. + ( + uint256 totalBaseSupplyBefore, + uint256 totalShareSupplyBefore + ) = getSupply(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalanceBefore = getAccountBalances( + address(hyperdrive) + ); + + // We normalize the sharesPaid variable within a valid range the market + // can support. + uint256 maxSharesPaid; + if (config.isRebasing) { + maxSharesPaid = convertToShares( + IERC20(config.vaultSharesToken).balanceOf(bob) / 10 + ); + } else { + maxSharesPaid = IERC20(config.vaultSharesToken).balanceOf(bob) / 10; + } + sharesPaid = sharesPaid.normalizeToRange( + convertToShares( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount + ), + maxSharesPaid + ); + + // Convert the amount to deposit in shares to the equivalent amount of + // base paid. + uint256 basePaid = convertToBase(sharesPaid); + + // Bob mints the bonds. We expect this to fail with an `UnsupportedToken` + // error if depositing with shares are not supported. + vm.startPrank(bob); + if (!config.enableShareDeposits) { + vm.expectRevert(IHyperdrive.UnsupportedToken.selector); + } + (uint256 maturityTime, uint256 bondAmount) = hyperdrive.mint( + sharesPaid, + 0, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: false, + extraData: new bytes(0) + }) + ); + + // Ensure that Bob received the correct amount of bonds. + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + bob + ), + bondAmount + ); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + maturityTime + ), + bob + ), + bondAmount + ); + + // Ensure the deposit accounting is correct. + verifyDeposit( + bob, + basePaid, + false, + totalBaseSupplyBefore, + totalShareSupplyBefore, + bobBalancesBefore, + hyperdriveBalanceBefore + ); + } + + /// @dev Fuzz test to ensure deposit accounting is correct when minting + /// bonds with the base token. This test case is expected to fail if + /// base deposits are not supported. + /// @param basePaid Amount in terms of base to mint the bonds. + function test_mint_with_base(uint256 basePaid) external { + // Get balance information before opening a long. + ( + uint256 totalBaseSupplyBefore, + uint256 totalShareSupplyBefore + ) = getSupply(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalanceBefore = getAccountBalances( + address(hyperdrive) + ); + + // If base deposits aren't enabled, we verify that ETH can't be sent to + // `mint` and that `mint` can't be called with `asBase = true`. + vm.startPrank(bob); + if (!config.enableBaseDeposits) { + // Set basePaid to a non-zero amount. + basePaid = 2 * hyperdrive.getPoolConfig().minimumTransactionAmount; + + // Check the `NotPayable` route. + vm.expectRevert(IHyperdrive.NotPayable.selector); + hyperdrive.mint{ value: basePaid }( + basePaid, + 0, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + + // Check the `UnsupportedToken` route. + vm.expectRevert(IHyperdrive.UnsupportedToken.selector); + hyperdrive.mint( + basePaid, + 0, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + + return; + } + + // Calculate the maximum amount of basePaid we can test. The limit is + // the amount of the base token that the trader has. + uint256 maxBaseAmount; + if (isBaseETH) { + maxBaseAmount = bob.balance; + } else { + maxBaseAmount = IERC20(config.baseToken).balanceOf(bob) / 10; + } + + // We normalize the basePaid variable within a valid range the market can support. + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + maxBaseAmount + ); + + // Bob mints some bonds by depositing the base token. + if (!isBaseETH) { + IERC20(hyperdrive.baseToken()).approve( + address(hyperdrive), + basePaid + ); + } + (uint256 maturityTime, uint256 bondAmount) = hyperdrive.mint{ + value: isBaseETH ? basePaid : 0 + }( + basePaid, + 0, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + + // Ensure that Bob received the correct amount of bonds. + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + bob + ), + bondAmount + ); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + maturityTime + ), + bob + ), + bondAmount + ); + + // Ensure the deposit accounting is correct. + verifyDeposit( + bob, + basePaid, + true, + totalBaseSupplyBefore, + totalShareSupplyBefore, + bobBalancesBefore, + hyperdriveBalanceBefore + ); + } + + // /// @dev Fuzz test to ensure withdrawal accounting is correct when closing + // /// longs with the share token. This test case is expected to fail if + // /// share withdraws are not supported. + // /// @param basePaid Amount in terms of base. + // /// @param variableRate Rate of interest accrual over the position duration. + // function test_close_long_with_shares( + // uint256 basePaid, + // int256 variableRate + // ) external virtual { + // // Early termination if share withdrawals are not supported. + // if (!config.enableShareWithdraws) { + // return; + // } + + // // Get Bob's account balances before opening the long. + // AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + + // // Accrue interest for a term. + // if (config.shouldAccrueInterest) { + // advanceTime( + // hyperdrive.getPoolConfig().positionDuration, + // int256(FIXED_RATE) + // ); + // } else { + // advanceTime(hyperdrive.getPoolConfig().positionDuration, 0); + // } + + // // Calculate the maximum amount of basePaid we can test. The limit is + // // either the maximum long that Hyperdrive can open or the amount of the + // // share token the trader has. + // uint256 maxLongAmount = HyperdriveUtils.calculateMaxLong(hyperdrive); + // uint256 maxShareAmount = bobBalancesBefore.sharesBalance; + + // // We normalize the basePaid variable within a valid range the market can support. + // basePaid = basePaid.normalizeToRange( + // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + // maxLongAmount > maxShareAmount ? maxShareAmount : maxLongAmount + // ); + + // // Bob opens a long with the share token. + // (uint256 maturityTime, uint256 longAmount) = openLong( + // bob, + // convertToShares(basePaid), + // false + // ); + + // // The term passes and some interest accrues. + // if (config.shouldAccrueInterest) { + // variableRate = variableRate.normalizeToRange(0, 2.5e18); + // } else { + // variableRate = 0; + // } + // advanceTime(hyperdrive.getPoolConfig().positionDuration, variableRate); + + // // Get some balance information before closing the long. + // ( + // uint256 totalBaseSupplyBefore, + // uint256 totalShareSupplyBefore + // ) = getSupply(); + // bobBalancesBefore = getAccountBalances(bob); + // AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + // address(hyperdrive) + // ); + + // // Bob closes his long with shares as the target asset. + // uint256 shareProceeds = closeLong(bob, maturityTime, longAmount, false); + // uint256 baseProceeds = convertToBase(shareProceeds); + + // // Ensure that Bob received approximately the bond amount but wasn't + // // overpaid. + // assertLe( + // baseProceeds, + // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat) + // ); + // assertApproxEqAbs( + // baseProceeds, + // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat), + // config.closeLongWithSharesTolerance + // ); + + // // Ensure the withdrawal accounting is correct. + // verifyWithdrawal( + // bob, + // baseProceeds, + // false, + // totalBaseSupplyBefore, + // totalShareSupplyBefore, + // bobBalancesBefore, + // hyperdriveBalancesBefore + // ); + // } + + // /// @dev Fuzz test to ensure withdrawal accounting is correct when closing + // /// longs with the base token. This test case is expected to fail if + // /// base withdraws are not supported. + // /// @param basePaid Amount in terms of base. + // /// @param variableRate Rate of interest accrual over the position duration. + // function test_close_long_with_base( + // uint256 basePaid, + // int256 variableRate + // ) external { + // // Get Bob's account balances before opening the long. + // AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + + // // Accrue interest for a term. + // if (config.shouldAccrueInterest) { + // advanceTime( + // hyperdrive.getPoolConfig().positionDuration, + // int256(FIXED_RATE) + // ); + // } else { + // advanceTime(hyperdrive.getPoolConfig().positionDuration, 0); + // } + + // // Calculate the maximum amount of basePaid we can test. The limit is + // // either the maximum long that Hyperdrive can open or the amount of the + // // base token the trader has. + // uint256 maxLongAmount = HyperdriveUtils.calculateMaxLong(hyperdrive); + + // // Open a long in either base or shares, depending on which asset is + // // supported. + // uint256 maturityTime; + // uint256 longAmount; + // if (config.enableBaseDeposits) { + // // We normalize the basePaid variable within a valid range the market + // // can support. + // uint256 maxBaseAmount = bobBalancesBefore.baseBalance; + // basePaid = basePaid.normalizeToRange( + // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + // maxLongAmount > maxBaseAmount ? maxBaseAmount : maxLongAmount + // ); + + // // Bob opens a long with the base token. + // (maturityTime, longAmount) = openLong(bob, basePaid); + // } else { + // // We normalize the sharesPaid variable within a valid range the market + // // can support. + // uint256 maxSharesAmount = bobBalancesBefore.sharesBalance; + // basePaid = basePaid.normalizeToRange( + // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + // maxLongAmount + // ); + + // // Bob opens a long with the share token. + // (maturityTime, longAmount) = openLong( + // bob, + // convertToShares(basePaid).min(maxSharesAmount), + // false + // ); + // } + + // // The term passes and some interest accrues. + // if (config.shouldAccrueInterest) { + // variableRate = variableRate.normalizeToRange(0, 2.5e18); + // } else { + // variableRate = 0; + // } + // advanceTime(hyperdrive.getPoolConfig().positionDuration, variableRate); + + // // Get some balance information before closing the long. + // ( + // uint256 totalBaseSupplyBefore, + // uint256 totalShareSupplyBefore + // ) = getSupply(); + // bobBalancesBefore = getAccountBalances(bob); + // AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + // address(hyperdrive) + // ); + + // // Bob closes the long. We expect to fail if withdrawing with base is + // // not supported. + // vm.startPrank(bob); + // if (!config.enableBaseWithdraws) { + // vm.expectRevert(config.baseWithdrawError); + // } + // uint256 baseProceeds = hyperdrive.closeLong( + // maturityTime, + // longAmount, + // 0, + // IHyperdrive.Options({ + // destination: bob, + // asBase: true, + // extraData: new bytes(0) + // }) + // ); + + // // Early termination if base withdraws are not supported. + // if (!config.enableBaseWithdraws) { + // return; + // } + + // // Ensure Bob is credited the correct amount of bonds. + // assertLe( + // baseProceeds, + // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat) + // ); + // assertApproxEqAbs( + // baseProceeds, + // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat), + // config.closeLongWithBaseTolerance + // ); + + // // Ensure the withdrawal accounting is correct. + // verifyWithdrawal( + // bob, + // baseProceeds, + // true, + // totalBaseSupplyBefore, + // totalShareSupplyBefore, + // bobBalancesBefore, + // hyperdriveBalancesBefore + // ); + // } + + // /// @dev Fuzz test that ensures that longs receive the correct payouts if + // /// they open and close instantaneously when deposits and withdrawals + // /// are made with base. + // /// @param _basePaid The fuzz parameter for the base paid. + // function test_round_trip_long_instantaneous_with_base( + // uint256 _basePaid + // ) external { + // // If base deposits aren't enabled, we skip the test. + // if (!config.enableBaseDeposits) { + // return; + // } + + // // Bob opens a long with base. + // _basePaid = _basePaid.normalizeToRange( + // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + // hyperdrive.calculateMaxLong() + // ); + // (uint256 maturityTime, uint256 longAmount) = openLong(bob, _basePaid); + + // // Get some balance information before the withdrawal. + // ( + // uint256 totalSupplyAssetsBefore, + // uint256 totalSupplySharesBefore + // ) = getSupply(); + // AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + // AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + // address(hyperdrive) + // ); + + // // If base withdrawals are supported, we withdraw with base. + // uint256 baseProceeds; + // if (config.enableBaseWithdraws) { + // // Bob closes his long with base as the target asset. + // baseProceeds = closeLong(bob, maturityTime, longAmount); + + // // Bob should receive less base than he paid since no time as passed. + // assertLt( + // baseProceeds, + // _basePaid + + // config.roundTripLongInstantaneousWithBaseUpperBoundTolerance + // ); + // // NOTE: If the fees aren't zero, we can't make an equality comparison. + // if (hyperdrive.getPoolConfig().fees.curve == 0) { + // assertApproxEqAbs( + // baseProceeds, + // _basePaid, + // config.roundTripLongInstantaneousWithBaseTolerance + // ); + // } + // } + // // Otherwise we withdraw with vault shares. + // else { + // // Bob closes his long with vault shares as the target asset. + // uint256 vaultSharesProceeds = closeLong( + // bob, + // maturityTime, + // longAmount, + // false + // ); + // baseProceeds = hyperdrive.convertToBase(vaultSharesProceeds); + + // // NOTE: We add a slight buffer since the fees are zero. + // // + // // Bob should receive less base than he paid since no time as passed. + // assertLt( + // vaultSharesProceeds, + // hyperdrive.convertToShares(_basePaid) + + // config + // .roundTripLongInstantaneousWithSharesUpperBoundTolerance + // ); + // // NOTE: If the fees aren't zero, we can't make an equality comparison. + // if (hyperdrive.getPoolConfig().fees.curve == 0) { + // assertApproxEqAbs( + // vaultSharesProceeds, + // hyperdrive.convertToShares(_basePaid), + // config.roundTripLongInstantaneousWithSharesTolerance + // ); + // } + // } + + // // Ensure that the withdrawal was processed as expected. + // verifyWithdrawal( + // bob, + // baseProceeds, + // config.enableBaseWithdraws, + // totalSupplyAssetsBefore, + // totalSupplySharesBefore, + // bobBalancesBefore, + // hyperdriveBalancesBefore + // ); + // } + + // /// @dev Fuzz test that ensures that longs receive the correct payouts if + // /// they open and close instantaneously when deposits and withdrawals + // /// are made with vault shares. + // /// @param _vaultSharesPaid The fuzz parameter for the vault shares paid. + // function test_round_trip_long_instantaneous_with_shares( + // uint256 _vaultSharesPaid + // ) external { + // // If share deposits aren't enabled, we skip the test. + // if (!config.enableShareDeposits) { + // return; + // } + + // // Bob opens a long with vault shares. + // _vaultSharesPaid = hyperdrive.convertToShares( + // _vaultSharesPaid.normalizeToRange( + // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + // hyperdrive.calculateMaxLong() + // ) + // ); + // (uint256 maturityTime, uint256 longAmount) = openLong( + // bob, + // _vaultSharesPaid, + // false + // ); + + // // Get some balance information before the withdrawal. + // ( + // uint256 totalSupplyAssetsBefore, + // uint256 totalSupplySharesBefore + // ) = getSupply(); + // AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + // AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + // address(hyperdrive) + // ); + + // // If vault share withdrawals are supported, we withdraw with vault + // // shares. + // uint256 baseProceeds; + // if (config.enableShareWithdraws) { + // // Bob closes his long with vault shares as the target asset. + // uint256 vaultSharesProceeds = closeLong( + // bob, + // maturityTime, + // longAmount, + // false + // ); + // baseProceeds = hyperdrive.convertToBase(vaultSharesProceeds); + + // // Bob should receive less base than he paid since no time as passed. + // assertLt( + // vaultSharesProceeds, + // _vaultSharesPaid + + // config + // .roundTripLongInstantaneousWithSharesUpperBoundTolerance + // ); + // // NOTE: If the fees aren't zero, we can't make an equality comparison. + // if (hyperdrive.getPoolConfig().fees.curve == 0) { + // assertApproxEqAbs( + // vaultSharesProceeds, + // _vaultSharesPaid, + // config.roundTripLongInstantaneousWithSharesTolerance + // ); + // } + // } + // // Otherwise we withdraw with base. + // else { + // // Bob closes his long with base as the target asset. + // baseProceeds = closeLong(bob, maturityTime, longAmount); + + // // Bob should receive less base than he paid since no time as passed. + // assertLt( + // baseProceeds, + // hyperdrive.convertToBase(_vaultSharesPaid) + + // config.roundTripLongInstantaneousWithBaseUpperBoundTolerance + // ); + // // NOTE: If the fees aren't zero, we can't make an equality comparison. + // if (hyperdrive.getPoolConfig().fees.curve == 0) { + // assertApproxEqAbs( + // baseProceeds, + // hyperdrive.convertToBase(_vaultSharesPaid), + // config.roundTripLongInstantaneousWithBaseTolerance + // ); + // } + // } + + // // Ensure that the withdrawal was processed as expected. + // verifyWithdrawal( + // bob, + // baseProceeds, + // !config.enableShareWithdraws, + // totalSupplyAssetsBefore, + // totalSupplySharesBefore, + // bobBalancesBefore, + // hyperdriveBalancesBefore + // ); + // } + + // /// @dev Fuzz test that ensures that shorts receive the correct payouts at + // /// maturity when deposits and withdrawals are made with base. + // /// @param _basePaid The fuzz parameter for the base paid. + // /// @param _variableRate The fuzz parameter for the variable rate. + // function test_round_trip_long_maturity_with_base( + // uint256 _basePaid, + // uint256 _variableRate + // ) external { + // // If base deposits aren't enabled, we skip the test. + // if (!config.enableBaseDeposits) { + // return; + // } + + // // Bob opens a long with base. + // _basePaid = _basePaid.normalizeToRange( + // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + // hyperdrive.calculateMaxLong() + // ); + // (uint256 maturityTime, uint256 longAmount) = openLong(bob, _basePaid); + + // // Advance the time and accrue a large amount of interest. + // if (config.shouldAccrueInterest) { + // _variableRate = _variableRate.normalizeToRange(0, 1000e18); + // } else { + // _variableRate = 0; + // } + // advanceTime(POSITION_DURATION, int256(_variableRate)); + + // // Get some balance information before the withdrawal. + // ( + // uint256 totalSupplyAssetsBefore, + // uint256 totalSupplySharesBefore + // ) = getSupply(); + // AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + // AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + // address(hyperdrive) + // ); + + // // If base withdrawals are supported, we withdraw with base. + // uint256 baseProceeds; + // if (config.enableBaseWithdraws) { + // // Bob closes his long with base as the target asset. + // baseProceeds = closeLong(bob, maturityTime, longAmount); + + // // Bob should receive almost exactly his bond amount. + // assertLe( + // baseProceeds, + // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat) + + // config.roundTripLongMaturityWithBaseUpperBoundTolerance + // ); + // assertApproxEqAbs( + // baseProceeds, + // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat), + // config.roundTripLongMaturityWithBaseTolerance + // ); + // } + // // Otherwise we withdraw with vault shares. + // else { + // // Bob closes his long with vault shares as the target asset. + // uint256 vaultSharesProceeds = closeLong( + // bob, + // maturityTime, + // longAmount, + // false + // ); + // baseProceeds = hyperdrive.convertToBase(vaultSharesProceeds); + + // // Bob should receive almost exactly his bond amount. + // assertLe( + // baseProceeds, + // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat) + + // config.roundTripLongMaturityWithSharesUpperBoundTolerance + // ); + // assertApproxEqAbs( + // baseProceeds, + // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat), + // config.roundTripLongMaturityWithSharesTolerance + // ); + // } + + // // Ensure that the withdrawal was processed as expected. + // verifyWithdrawal( + // bob, + // baseProceeds, + // config.enableBaseWithdraws, + // totalSupplyAssetsBefore, + // totalSupplySharesBefore, + // bobBalancesBefore, + // hyperdriveBalancesBefore + // ); + // } + + // /// @dev Fuzz test that ensures that shorts receive the correct payouts at + // /// maturity when deposits and withdrawals are made with vault shares. + // /// @param _vaultSharesPaid The fuzz parameter for the vault shares paid. + // /// @param _variableRate The fuzz parameter for the variable rate. + // function test_round_trip_long_maturity_with_shares( + // uint256 _vaultSharesPaid, + // uint256 _variableRate + // ) external { + // // If share deposits aren't enabled, we skip the test. + // if (!config.enableShareDeposits) { + // return; + // } + + // // Bob opens a long with vault shares. + // _vaultSharesPaid = hyperdrive.convertToShares( + // _vaultSharesPaid.normalizeToRange( + // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + // hyperdrive.calculateMaxLong() + // ) + // ); + // (uint256 maturityTime, uint256 longAmount) = openLong( + // bob, + // _vaultSharesPaid, + // false + // ); + + // // Advance the time and accrue a large amount of interest. + // if (config.shouldAccrueInterest) { + // _variableRate = _variableRate.normalizeToRange(0, 1000e18); + // } else { + // _variableRate = 0; + // } + // advanceTime( + // hyperdrive.getPoolConfig().positionDuration, + // int256(_variableRate) + // ); + + // // Get some balance information before the withdrawal. + // ( + // uint256 totalSupplyAssetsBefore, + // uint256 totalSupplySharesBefore + // ) = getSupply(); + // AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + // AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + // address(hyperdrive) + // ); + + // // If vault share withdrawals are supported, we withdraw with vault + // // shares. + // uint256 baseProceeds; + // if (config.enableShareWithdraws) { + // // Bob closes his long with vault shares as the target asset. + // uint256 vaultSharesProceeds = closeLong( + // bob, + // maturityTime, + // longAmount, + // false + // ); + // baseProceeds = hyperdrive.convertToBase(vaultSharesProceeds); + + // // Bob should receive almost exactly his bond amount. + // assertLe( + // baseProceeds, + // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat) + + // config.roundTripLongMaturityWithSharesUpperBoundTolerance + // ); + // assertApproxEqAbs( + // baseProceeds, + // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat), + // config.roundTripLongMaturityWithSharesTolerance + // ); + // } + // // Otherwise we withdraw with base. + // else { + // // Bob closes his long with base as the target asset. + // baseProceeds = closeLong(bob, maturityTime, longAmount); + + // // Bob should receive almost exactly his bond amount. + // assertLe( + // baseProceeds, + // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat) + + // config.roundTripLongMaturityWithBaseUpperBoundTolerance + // ); + // assertApproxEqAbs( + // baseProceeds, + // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat), + // config.roundTripLongMaturityWithBaseTolerance + // ); + // } + + // // Ensure that the withdrawal was processed as expected. + // verifyWithdrawal( + // bob, + // baseProceeds, + // !config.enableShareWithdraws, + // totalSupplyAssetsBefore, + // totalSupplySharesBefore, + // bobBalancesBefore, + // hyperdriveBalancesBefore + // ); + // } + /// Sweep /// function test_sweep_failure_directSweep() external { From 324beb384f67e4f19330b6817f02e9ceabfd4ab9 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 10 Jan 2025 18:11:17 -0600 Subject: [PATCH 30/45] Added more `mint` and `burn` related cases to the `InstanceTest` suite --- test/instances/ezETH/EzETHHyperdrive.t.sol | 2 +- test/utils/InstanceTest.sol | 448 ++++++++++++--------- 2 files changed, 252 insertions(+), 198 deletions(-) diff --git a/test/instances/ezETH/EzETHHyperdrive.t.sol b/test/instances/ezETH/EzETHHyperdrive.t.sol index 5490c3ed7..7e48447e6 100644 --- a/test/instances/ezETH/EzETHHyperdrive.t.sol +++ b/test/instances/ezETH/EzETHHyperdrive.t.sol @@ -112,7 +112,7 @@ contract EzETHHyperdriveTest is InstanceTest { roundTripShortMaturityWithSharesTolerance: 1e8, // The verification tolerances. verifyDepositTolerance: 300, - verifyWithdrawalTolerance: 1_000 + verifyWithdrawalTolerance: 3_000 }) ) {} diff --git a/test/utils/InstanceTest.sol b/test/utils/InstanceTest.sol index 726e3e648..62b9c7212 100644 --- a/test/utils/InstanceTest.sol +++ b/test/utils/InstanceTest.sol @@ -1,9 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; -// FIXME -import { console2 as console } from "forge-std/console2.sol"; - import { ERC20ForwarderFactory } from "../../contracts/src/token/ERC20ForwarderFactory.sol"; import { HyperdriveFactory } from "../../contracts/src/factory/HyperdriveFactory.sol"; import { IERC20 } from "../../contracts/src/interfaces/IERC20.sol"; @@ -3135,8 +3132,6 @@ abstract contract InstanceTest is HyperdriveTest { } } - // FIXME: Make some of these updates to the other instance tests. - // /// @dev Fuzz test to ensure deposit accounting is correct when mint bonds /// with the share token. This test case is expected to fail if share /// deposits are not supported. @@ -3347,220 +3342,279 @@ abstract contract InstanceTest is HyperdriveTest { ); } - // /// @dev Fuzz test to ensure withdrawal accounting is correct when closing - // /// longs with the share token. This test case is expected to fail if - // /// share withdraws are not supported. - // /// @param basePaid Amount in terms of base. - // /// @param variableRate Rate of interest accrual over the position duration. - // function test_close_long_with_shares( - // uint256 basePaid, - // int256 variableRate - // ) external virtual { - // // Early termination if share withdrawals are not supported. - // if (!config.enableShareWithdraws) { - // return; - // } + /// @dev Fuzz test to ensure withdrawal accounting is correct when burning + /// bonds with the share token. This test case is expected to fail if + /// share withdraws are not supported. + /// @param sharesPaid Amount paid in shares. + /// @param variableRate Rate of interest accrual over the position duration. + function test_burn_with_shares( + uint256 sharesPaid, + int256 variableRate + ) external virtual { + // Early termination if share withdrawals are not supported. + if (!config.enableShareWithdraws) { + return; + } - // // Get Bob's account balances before opening the long. - // AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + // Get Bob's account balances before opening the long. + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); - // // Accrue interest for a term. - // if (config.shouldAccrueInterest) { - // advanceTime( - // hyperdrive.getPoolConfig().positionDuration, - // int256(FIXED_RATE) - // ); - // } else { - // advanceTime(hyperdrive.getPoolConfig().positionDuration, 0); - // } + // Accrue interest for a term. + if (config.shouldAccrueInterest) { + advanceTime( + hyperdrive.getPoolConfig().positionDuration, + int256(FIXED_RATE) + ); + } else { + advanceTime(hyperdrive.getPoolConfig().positionDuration, 0); + } - // // Calculate the maximum amount of basePaid we can test. The limit is - // // either the maximum long that Hyperdrive can open or the amount of the - // // share token the trader has. - // uint256 maxLongAmount = HyperdriveUtils.calculateMaxLong(hyperdrive); - // uint256 maxShareAmount = bobBalancesBefore.sharesBalance; + // We normalize the sharesPaid variable within a valid range the market + // can support. + uint256 maxSharesPaid; + if (config.isRebasing) { + maxSharesPaid = convertToShares( + IERC20(config.vaultSharesToken).balanceOf(bob) / 10 + ); + } else { + maxSharesPaid = IERC20(config.vaultSharesToken).balanceOf(bob) / 10; + } + sharesPaid = sharesPaid.normalizeToRange( + convertToShares( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount + ), + maxSharesPaid + ); - // // We normalize the basePaid variable within a valid range the market can support. - // basePaid = basePaid.normalizeToRange( - // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, - // maxLongAmount > maxShareAmount ? maxShareAmount : maxLongAmount - // ); + // Bob mints bonds with the share token. + (uint256 maturityTime, uint256 bondAmount) = mint( + bob, + sharesPaid, + false + ); - // // Bob opens a long with the share token. - // (uint256 maturityTime, uint256 longAmount) = openLong( - // bob, - // convertToShares(basePaid), - // false - // ); + // The term passes and some interest accrues. + if (config.shouldAccrueInterest) { + variableRate = variableRate.normalizeToRange(0, 2.5e18); + } else { + variableRate = 0; + } + advanceTime(hyperdrive.getPoolConfig().positionDuration, variableRate); - // // The term passes and some interest accrues. - // if (config.shouldAccrueInterest) { - // variableRate = variableRate.normalizeToRange(0, 2.5e18); - // } else { - // variableRate = 0; - // } - // advanceTime(hyperdrive.getPoolConfig().positionDuration, variableRate); + // Get some balance information before burning the bonds. + ( + uint256 totalBaseSupplyBefore, + uint256 totalShareSupplyBefore + ) = getSupply(); + bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); - // // Get some balance information before closing the long. - // ( - // uint256 totalBaseSupplyBefore, - // uint256 totalShareSupplyBefore - // ) = getSupply(); - // bobBalancesBefore = getAccountBalances(bob); - // AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( - // address(hyperdrive) - // ); + // Bob closes his long with shares as the target asset. + uint256 shareProceeds = burn(bob, maturityTime, bondAmount, false); + uint256 baseProceeds = convertToBase(shareProceeds); - // // Bob closes his long with shares as the target asset. - // uint256 shareProceeds = closeLong(bob, maturityTime, longAmount, false); - // uint256 baseProceeds = convertToBase(shareProceeds); + // Ensure that Bob received approximately the value of the underlying + // bonds (minus fees) but wasn't overpaid. + uint256 openVaultSharePrice = hyperdrive + .getCheckpoint( + maturityTime - hyperdrive.getPoolConfig().positionDuration + ) + .vaultSharePrice; + uint256 closeVaultSharePrice = hyperdrive + .getCheckpoint(maturityTime) + .vaultSharePrice; + uint256 expectedBaseProceeds = bondAmount.mulDivDown( + closeVaultSharePrice, + openVaultSharePrice + ) - + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + ONE - hyperdrive.getPoolConfig().fees.governanceLP + ) - + 2 * + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + hyperdrive.getPoolConfig().fees.governanceLP + ); + assertLe(baseProceeds, expectedBaseProceeds); + assertApproxEqAbs( + baseProceeds, + expectedBaseProceeds, + config.closeLongWithSharesTolerance + ); - // // Ensure that Bob received approximately the bond amount but wasn't - // // overpaid. - // assertLe( - // baseProceeds, - // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat) - // ); - // assertApproxEqAbs( - // baseProceeds, - // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat), - // config.closeLongWithSharesTolerance - // ); + // Ensure the withdrawal accounting is correct. + AccountBalances memory bobBalancesBefore_ = bobBalancesBefore; // avoid stack-too-deep + verifyWithdrawal( + bob, + baseProceeds, + false, + totalBaseSupplyBefore, + totalShareSupplyBefore, + bobBalancesBefore_, + hyperdriveBalancesBefore + ); + } - // // Ensure the withdrawal accounting is correct. - // verifyWithdrawal( - // bob, - // baseProceeds, - // false, - // totalBaseSupplyBefore, - // totalShareSupplyBefore, - // bobBalancesBefore, - // hyperdriveBalancesBefore - // ); - // } + // FIXME: This failed in a bunch of places. + // + /// @dev Fuzz test to ensure withdrawal accounting is correct when burning + /// bonds with the base token. This test case is expected to fail if + /// base withdraws are not supported. + /// @param basePaid Amount in terms of base. + /// @param variableRate Rate of interest accrual over the position duration. + function test_burn_with_base( + uint256 basePaid, + int256 variableRate + ) external { + // Get Bob's account balances before opening the long. + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); - // /// @dev Fuzz test to ensure withdrawal accounting is correct when closing - // /// longs with the base token. This test case is expected to fail if - // /// base withdraws are not supported. - // /// @param basePaid Amount in terms of base. - // /// @param variableRate Rate of interest accrual over the position duration. - // function test_close_long_with_base( - // uint256 basePaid, - // int256 variableRate - // ) external { - // // Get Bob's account balances before opening the long. - // AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + // Accrue interest for a term. + if (config.shouldAccrueInterest) { + advanceTime( + hyperdrive.getPoolConfig().positionDuration, + int256(FIXED_RATE) + ); + } else { + advanceTime(hyperdrive.getPoolConfig().positionDuration, 0); + } - // // Accrue interest for a term. - // if (config.shouldAccrueInterest) { - // advanceTime( - // hyperdrive.getPoolConfig().positionDuration, - // int256(FIXED_RATE) - // ); - // } else { - // advanceTime(hyperdrive.getPoolConfig().positionDuration, 0); - // } + // Mint bonds in either base or shares, depending on which asset is + // supported. + uint256 maturityTime; + uint256 bondAmount; + if (config.enableBaseDeposits) { + // Calculate the maximum amount of basePaid we can test. The limit is + // the amount of the base token that the trader has. + uint256 maxBaseAmount; + if (isBaseETH) { + maxBaseAmount = bob.balance; + } else { + maxBaseAmount = IERC20(config.baseToken).balanceOf(bob) / 10; + } - // // Calculate the maximum amount of basePaid we can test. The limit is - // // either the maximum long that Hyperdrive can open or the amount of the - // // base token the trader has. - // uint256 maxLongAmount = HyperdriveUtils.calculateMaxLong(hyperdrive); - - // // Open a long in either base or shares, depending on which asset is - // // supported. - // uint256 maturityTime; - // uint256 longAmount; - // if (config.enableBaseDeposits) { - // // We normalize the basePaid variable within a valid range the market - // // can support. - // uint256 maxBaseAmount = bobBalancesBefore.baseBalance; - // basePaid = basePaid.normalizeToRange( - // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, - // maxLongAmount > maxBaseAmount ? maxBaseAmount : maxLongAmount - // ); + // We normalize the basePaid variable within a valid range the market + // can support. + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + maxBaseAmount + ); - // // Bob opens a long with the base token. - // (maturityTime, longAmount) = openLong(bob, basePaid); - // } else { - // // We normalize the sharesPaid variable within a valid range the market - // // can support. - // uint256 maxSharesAmount = bobBalancesBefore.sharesBalance; - // basePaid = basePaid.normalizeToRange( - // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, - // maxLongAmount - // ); + // Bob mints bonds with the base token. + (maturityTime, bondAmount) = mint(bob, basePaid); + } else { + // Calculate the maximum amount of sharesPaid we can test. The limit + // is the amount of the vault shares token that the trader has. + uint256 maxSharesAmount = IERC20(config.vaultSharesToken).balanceOf( + bob + ) / 10; - // // Bob opens a long with the share token. - // (maturityTime, longAmount) = openLong( - // bob, - // convertToShares(basePaid).min(maxSharesAmount), - // false - // ); - // } + // We normalize the basePaid variable within a valid range the market + // can support. + if (config.isRebasing) { + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + maxSharesAmount + ); + } else { + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + convertToBase(maxSharesAmount) + ); + } + uint256 sharesPaid = convertToShares(basePaid); - // // The term passes and some interest accrues. - // if (config.shouldAccrueInterest) { - // variableRate = variableRate.normalizeToRange(0, 2.5e18); - // } else { - // variableRate = 0; - // } - // advanceTime(hyperdrive.getPoolConfig().positionDuration, variableRate); + // Bob mints bonds with the share token. + (maturityTime, bondAmount) = mint( + bob, + sharesPaid.min(maxSharesAmount), + false + ); + } - // // Get some balance information before closing the long. - // ( - // uint256 totalBaseSupplyBefore, - // uint256 totalShareSupplyBefore - // ) = getSupply(); - // bobBalancesBefore = getAccountBalances(bob); - // AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( - // address(hyperdrive) - // ); + // The term passes and some interest accrues. + if (config.shouldAccrueInterest) { + variableRate = variableRate.normalizeToRange(0, 2.5e18); + } else { + variableRate = 0; + } + advanceTime(hyperdrive.getPoolConfig().positionDuration, variableRate); - // // Bob closes the long. We expect to fail if withdrawing with base is - // // not supported. - // vm.startPrank(bob); - // if (!config.enableBaseWithdraws) { - // vm.expectRevert(config.baseWithdrawError); - // } - // uint256 baseProceeds = hyperdrive.closeLong( - // maturityTime, - // longAmount, - // 0, - // IHyperdrive.Options({ - // destination: bob, - // asBase: true, - // extraData: new bytes(0) - // }) - // ); + // Get some balance information before closing the long. + ( + uint256 totalBaseSupplyBefore, + uint256 totalShareSupplyBefore + ) = getSupply(); + bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); - // // Early termination if base withdraws are not supported. - // if (!config.enableBaseWithdraws) { - // return; - // } + // Bob burns the bonds. We expect to fail if withdrawing with base is + // not supported. + vm.startPrank(bob); + if (!config.enableBaseWithdraws) { + vm.expectRevert(config.baseWithdrawError); + } + uint256 baseProceeds = hyperdrive.burn( + maturityTime, + bondAmount, + 0, + IHyperdrive.Options({ + destination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); - // // Ensure Bob is credited the correct amount of bonds. - // assertLe( - // baseProceeds, - // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat) - // ); - // assertApproxEqAbs( - // baseProceeds, - // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat), - // config.closeLongWithBaseTolerance - // ); + // Early termination if base withdraws are not supported. + if (!config.enableBaseWithdraws) { + return; + } - // // Ensure the withdrawal accounting is correct. - // verifyWithdrawal( - // bob, - // baseProceeds, - // true, - // totalBaseSupplyBefore, - // totalShareSupplyBefore, - // bobBalancesBefore, - // hyperdriveBalancesBefore - // ); - // } + // Ensure that Bob received approximately the value of the underlying + // bonds (minus fees) but wasn't overpaid. + uint256 openVaultSharePrice = hyperdrive + .getCheckpoint( + maturityTime - hyperdrive.getPoolConfig().positionDuration + ) + .vaultSharePrice; + uint256 closeVaultSharePrice = hyperdrive + .getCheckpoint(maturityTime) + .vaultSharePrice; + uint256 expectedBaseProceeds = bondAmount.mulDivDown( + closeVaultSharePrice, + openVaultSharePrice + ) - + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + ONE - hyperdrive.getPoolConfig().fees.governanceLP + ) - + 2 * + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + hyperdrive.getPoolConfig().fees.governanceLP + ); + assertLe(baseProceeds, expectedBaseProceeds); + assertApproxEqAbs( + baseProceeds, + expectedBaseProceeds, + config.closeLongWithSharesTolerance + ); + // Ensure the withdrawal accounting is correct. + verifyWithdrawal( + bob, + baseProceeds, + true, + totalBaseSupplyBefore, + totalShareSupplyBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + // FIXME + // // /// @dev Fuzz test that ensures that longs receive the correct payouts if // /// they open and close instantaneously when deposits and withdrawals // /// are made with base. From cf64c36d5117138b9852d1fdcdb8b86e027cae42 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 15 Jan 2025 14:44:30 -1000 Subject: [PATCH 31/45] Fixed the failing `test_burn_with_base` tests --- test/instances/erc4626/sUSDS.t.sol | 12 +++++++++++- .../MorphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol | 6 +----- test/utils/InstanceTest.sol | 4 +--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/test/instances/erc4626/sUSDS.t.sol b/test/instances/erc4626/sUSDS.t.sol index 50c1d2c21..8c4c6d2a0 100644 --- a/test/instances/erc4626/sUSDS.t.sol +++ b/test/instances/erc4626/sUSDS.t.sol @@ -120,19 +120,29 @@ contract sUSDSHyperdriveTest is ERC4626HyperdriveInstanceTest { uint256 timeDelta, int256 variableRate ) internal override { + uint256 baseBalance = USDS.balanceOf(address(SUSDS)); uint256 chi = ISUSDS(address(SUSDS)).chi(); uint256 rho = ISUSDS(address(SUSDS)).rho(); uint256 ssr = ISUSDS(address(SUSDS)).ssr(); chi = (_rpow(ssr, block.timestamp - rho) * chi) / RAY; // Accrue interest in the sUSDS market. This amounts to manually - // updating the total supply assets. + // updating the total supply assets and increasing the contract's + // USDS balance. (chi, ) = chi.calculateInterest(variableRate, timeDelta); + (baseBalance, ) = baseBalance.calculateInterest( + variableRate, + timeDelta + ); // Advance the time. vm.warp(block.timestamp + timeDelta); // Update the sUSDS market state. + bytes32 balanceLocation = keccak256(abi.encode(address(SUSDS), 2)); + vm.store(address(USDS), balanceLocation, bytes32(baseBalance)); + + // Update the sUSDS contract's base balance. vm.store( address(SUSDS), bytes32(uint256(5)), diff --git a/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol index 52289c5bd..23e6c69d1 100644 --- a/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol @@ -129,10 +129,6 @@ contract MorphoBlue_cbETH_USDC_Base_HyperdriveTest is uint256 _amount ) internal override { bytes32 balanceLocation = keccak256(abi.encode(address(_recipient), 9)); - vm.store( - IFiatTokenProxy(LOAN_TOKEN).implementation(), - balanceLocation, - bytes32(_amount) - ); + vm.store(LOAN_TOKEN, balanceLocation, bytes32(_amount)); } } diff --git a/test/utils/InstanceTest.sol b/test/utils/InstanceTest.sol index 62b9c7212..2eeab0356 100644 --- a/test/utils/InstanceTest.sol +++ b/test/utils/InstanceTest.sol @@ -3456,8 +3456,6 @@ abstract contract InstanceTest is HyperdriveTest { ); } - // FIXME: This failed in a bunch of places. - // /// @dev Fuzz test to ensure withdrawal accounting is correct when burning /// bonds with the base token. This test case is expected to fail if /// base withdraws are not supported. @@ -3598,7 +3596,7 @@ abstract contract InstanceTest is HyperdriveTest { assertApproxEqAbs( baseProceeds, expectedBaseProceeds, - config.closeLongWithSharesTolerance + config.closeLongWithBaseTolerance ); // Ensure the withdrawal accounting is correct. From 07afed0695b1ddc341fcb03b85cb1e158bd76797 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 16 Jan 2025 09:35:15 -1000 Subject: [PATCH 32/45] Added another instance test and got all of the tests working --- test/instances/aave/AaveHyperdrive.t.sol | 8 + test/instances/aave/AaveL2Hyperdrive.t.sol | 8 + .../AerodromeLp_AERO_USDC_Hyperdrive.t.sol | 8 + test/instances/chainlink/CbETHBase.t.sol | 14 +- .../chainlink/WstETHGnosisChain.t.sol | 14 +- test/instances/corn/Corn_LBTC_Hyperdrive.sol | 8 + .../instances/corn/Corn_sDAI_Hyperdrive.t.sol | 8 + test/instances/eeth/EETHHyperdrive.t.sol | 8 + test/instances/erc4626/MoonwellETH.t.sol | 8 + test/instances/erc4626/MoonwellEURC.t.sol | 8 + test/instances/erc4626/MoonwellUSDC.t.sol | 8 + test/instances/erc4626/SUSDe.t.sol | 10 + test/instances/erc4626/ScrvUSD.t.sol | 8 + test/instances/erc4626/SnARS.t.sol | 8 + test/instances/erc4626/StUSD.t.sol | 8 + test/instances/erc4626/sGYD.t.sol | 8 + test/instances/erc4626/sGYD_gnosis.t.sol | 8 + test/instances/erc4626/sUSDS.t.sol | 9 + test/instances/erc4626/sxDai.t.sol | 8 + test/instances/ezETH/EzETHHyperdrive.t.sol | 8 + .../ezeth-linea/EzETHLineaTest.t.sol | 8 + test/instances/lseth/LsETHHyperdrive.t.sol | 14 +- .../MorphoBlue_USDe_DAI_Hyperdrive.t.sol | 9 + .../MorphoBlue_WBTC_USDC_Hyperdrive.t.sol | 8 + ...orphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol | 8 + ...hoBlue_cbETH_USDC_Mainnet_Hyperdrive.t.sol | 8 + .../MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol | 9 + .../MorphoBlue_wstETH_USDA_Hyperdrive.t.sol | 9 + .../MorphoBlue_wstETH_USDC_Hyperdrive.t.sol | 8 + test/instances/reth/RETHHyperdrive.t.sol | 8 + .../rseth-linea/RsETHLineaHyperdrive.t.sol | 8 + .../SavingsUSDS_Base_Hyperdrive.t.sol | 8 + .../StakingUSDS_Chronicle_Hyperdrive.t.sol | 8 + .../StakingUSDS_Sky_Hyperdrive.t.sol | 8 + test/instances/steth/StETHHyperdrive.t.sol | 15 +- .../stk-well/StkWellHyperdrive.t.sol | 8 + test/utils/InstanceTest.sol | 209 ++++++++++-------- 37 files changed, 428 insertions(+), 100 deletions(-) diff --git a/test/instances/aave/AaveHyperdrive.t.sol b/test/instances/aave/AaveHyperdrive.t.sol index f768a3602..b158239b3 100644 --- a/test/instances/aave/AaveHyperdrive.t.sol +++ b/test/instances/aave/AaveHyperdrive.t.sol @@ -75,6 +75,7 @@ contract AaveHyperdriveTest is InstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 10, roundTripLpInstantaneousWithBaseTolerance: 1e5, roundTripLpWithdrawalSharesWithBaseTolerance: 1e5, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -84,9 +85,13 @@ contract AaveHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 10, roundTripLpInstantaneousWithSharesTolerance: 1e5, roundTripLpWithdrawalSharesWithSharesTolerance: 1e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -96,6 +101,9 @@ contract AaveHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/aave/AaveL2Hyperdrive.t.sol b/test/instances/aave/AaveL2Hyperdrive.t.sol index 12dcd93dc..2b014db73 100644 --- a/test/instances/aave/AaveL2Hyperdrive.t.sol +++ b/test/instances/aave/AaveL2Hyperdrive.t.sol @@ -85,6 +85,7 @@ contract AaveL2HyperdriveTest is InstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 10, roundTripLpInstantaneousWithBaseTolerance: 1e5, roundTripLpWithdrawalSharesWithBaseTolerance: 1e5, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -94,9 +95,13 @@ contract AaveL2HyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e5, roundTripLpWithdrawalSharesWithSharesTolerance: 1e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -106,6 +111,9 @@ contract AaveL2HyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/aerodrome/AerodromeLp_AERO_USDC_Hyperdrive.t.sol b/test/instances/aerodrome/AerodromeLp_AERO_USDC_Hyperdrive.t.sol index 101ec2f73..75eb09286 100644 --- a/test/instances/aerodrome/AerodromeLp_AERO_USDC_Hyperdrive.t.sol +++ b/test/instances/aerodrome/AerodromeLp_AERO_USDC_Hyperdrive.t.sol @@ -59,6 +59,7 @@ contract AerodromeLp_AERO_USDC_Hyperdrive is AerodromeLpHyperdriveInstanceTest { closeLongWithBaseTolerance: 0, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 0, roundTripLpInstantaneousWithBaseTolerance: 0, roundTripLpWithdrawalSharesWithBaseTolerance: 10, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 100, @@ -68,12 +69,16 @@ contract AerodromeLp_AERO_USDC_Hyperdrive is AerodromeLpHyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 100, roundTripShortInstantaneousWithBaseTolerance: 10, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 100, + roundTripPairInstantaneousWithBaseTolerance: 10, + roundTripPairMaturityWithBaseTolerance: 0, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. // // The share test tolerances. closeLongWithSharesTolerance: 0, closeShortWithSharesTolerance: 0, + burnWithSharesTolerance: 0, roundTripLpInstantaneousWithSharesTolerance: 0, roundTripLpWithdrawalSharesWithSharesTolerance: 0, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 0, @@ -83,6 +88,9 @@ contract AerodromeLp_AERO_USDC_Hyperdrive is AerodromeLpHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 3 diff --git a/test/instances/chainlink/CbETHBase.t.sol b/test/instances/chainlink/CbETHBase.t.sol index 882a1d576..5dd6b0aaf 100644 --- a/test/instances/chainlink/CbETHBase.t.sol +++ b/test/instances/chainlink/CbETHBase.t.sol @@ -55,9 +55,10 @@ contract CbETHBaseTest is ChainlinkHyperdriveInstanceTest { // tolerances are zero. // // The base test tolerances. - closeLongWithBaseTolerance: 20, - closeShortWithBaseUpperBoundTolerance: 10, - closeShortWithBaseTolerance: 100, + closeLongWithBaseTolerance: 0, + closeShortWithBaseUpperBoundTolerance: 0, + closeShortWithBaseTolerance: 0, + burnWithBaseTolerance: 0, roundTripLpInstantaneousWithBaseTolerance: 0, roundTripLpWithdrawalSharesWithBaseTolerance: 0, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 0, @@ -67,9 +68,13 @@ contract CbETHBaseTest is ChainlinkHyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e3, roundTripLpWithdrawalSharesWithSharesTolerance: 1e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e4, @@ -79,6 +84,9 @@ contract CbETHBaseTest is ChainlinkHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/chainlink/WstETHGnosisChain.t.sol b/test/instances/chainlink/WstETHGnosisChain.t.sol index bcb89041f..b74c0f774 100644 --- a/test/instances/chainlink/WstETHGnosisChain.t.sol +++ b/test/instances/chainlink/WstETHGnosisChain.t.sol @@ -56,9 +56,10 @@ contract WstETHGnosisChainTest is ChainlinkHyperdriveInstanceTest { // tolerances are zero. // // The base test tolerances. - closeLongWithBaseTolerance: 20, - closeShortWithBaseUpperBoundTolerance: 10, - closeShortWithBaseTolerance: 100, + closeLongWithBaseTolerance: 0, + closeShortWithBaseUpperBoundTolerance: 0, + closeShortWithBaseTolerance: 0, + burnWithBaseTolerance: 0, roundTripLpInstantaneousWithBaseTolerance: 0, roundTripLpWithdrawalSharesWithBaseTolerance: 0, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 0, @@ -68,9 +69,13 @@ contract WstETHGnosisChainTest is ChainlinkHyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e3, roundTripLpWithdrawalSharesWithSharesTolerance: 1e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -80,6 +85,9 @@ contract WstETHGnosisChainTest is ChainlinkHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/corn/Corn_LBTC_Hyperdrive.sol b/test/instances/corn/Corn_LBTC_Hyperdrive.sol index 69f10a480..62679a41c 100644 --- a/test/instances/corn/Corn_LBTC_Hyperdrive.sol +++ b/test/instances/corn/Corn_LBTC_Hyperdrive.sol @@ -57,6 +57,7 @@ contract Corn_LBTC_Hyperdrive is CornHyperdriveInstanceTest { closeLongWithBaseTolerance: 2, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 2, roundTripLpInstantaneousWithBaseTolerance: 1e3, roundTripLpWithdrawalSharesWithBaseTolerance: 1e5, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 100, @@ -68,12 +69,16 @@ contract Corn_LBTC_Hyperdrive is CornHyperdriveInstanceTest { // NOTE: Since the curve fee isn't zero, this check is ignored. roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 1e3, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 100, + roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. // // The share test tolerances. closeLongWithSharesTolerance: 0, closeShortWithSharesTolerance: 0, + burnWithSharesTolerance: 0, roundTripLpInstantaneousWithSharesTolerance: 0, roundTripLpWithdrawalSharesWithSharesTolerance: 0, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 0, @@ -83,6 +88,9 @@ contract Corn_LBTC_Hyperdrive is CornHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 3 diff --git a/test/instances/corn/Corn_sDAI_Hyperdrive.t.sol b/test/instances/corn/Corn_sDAI_Hyperdrive.t.sol index 4e578794c..b15556e5d 100644 --- a/test/instances/corn/Corn_sDAI_Hyperdrive.t.sol +++ b/test/instances/corn/Corn_sDAI_Hyperdrive.t.sol @@ -57,6 +57,7 @@ contract Corn_sDAI_Hyperdrive is CornHyperdriveInstanceTest { closeLongWithBaseTolerance: 2, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 2, roundTripLpInstantaneousWithBaseTolerance: 1e3, roundTripLpWithdrawalSharesWithBaseTolerance: 1e7, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -68,12 +69,16 @@ contract Corn_sDAI_Hyperdrive is CornHyperdriveInstanceTest { // NOTE: Since the curve fee isn't zero, this check is ignored. roundTripShortInstantaneousWithBaseTolerance: 1e3, roundTripShortMaturityWithBaseTolerance: 1e3, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. // // The share test tolerances. closeLongWithSharesTolerance: 0, closeShortWithSharesTolerance: 0, + burnWithSharesTolerance: 0, roundTripLpInstantaneousWithSharesTolerance: 0, roundTripLpWithdrawalSharesWithSharesTolerance: 0, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 0, @@ -83,6 +88,9 @@ contract Corn_sDAI_Hyperdrive is CornHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 3 diff --git a/test/instances/eeth/EETHHyperdrive.t.sol b/test/instances/eeth/EETHHyperdrive.t.sol index 5c5a80f42..5ad76df80 100644 --- a/test/instances/eeth/EETHHyperdrive.t.sol +++ b/test/instances/eeth/EETHHyperdrive.t.sol @@ -79,6 +79,7 @@ contract EETHHyperdriveTest is InstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 0, roundTripLpWithdrawalSharesWithBaseTolerance: 0, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 0, @@ -88,9 +89,13 @@ contract EETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e5, roundTripLpWithdrawalSharesWithSharesTolerance: 1e5, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -100,6 +105,9 @@ contract EETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e4, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e4, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/erc4626/MoonwellETH.t.sol b/test/instances/erc4626/MoonwellETH.t.sol index 0af2247f1..4da2d659b 100644 --- a/test/instances/erc4626/MoonwellETH.t.sol +++ b/test/instances/erc4626/MoonwellETH.t.sol @@ -55,6 +55,7 @@ contract MoonwellETHHyperdriveTest is MetaMorphoHyperdriveInstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 1e5, roundTripLpWithdrawalSharesWithBaseTolerance: 1e6, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -64,9 +65,13 @@ contract MoonwellETHHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -76,6 +81,9 @@ contract MoonwellETHHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/erc4626/MoonwellEURC.t.sol b/test/instances/erc4626/MoonwellEURC.t.sol index 716b9a897..66d33dd3f 100644 --- a/test/instances/erc4626/MoonwellEURC.t.sol +++ b/test/instances/erc4626/MoonwellEURC.t.sol @@ -62,6 +62,7 @@ contract MoonwellEURCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 1e5, roundTripLpWithdrawalSharesWithBaseTolerance: 1e6, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -71,9 +72,13 @@ contract MoonwellEURCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e16, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -83,6 +88,9 @@ contract MoonwellEURCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 1e12, verifyWithdrawalTolerance: 1e13 diff --git a/test/instances/erc4626/MoonwellUSDC.t.sol b/test/instances/erc4626/MoonwellUSDC.t.sol index d877b0ec2..d7d7ac9b3 100644 --- a/test/instances/erc4626/MoonwellUSDC.t.sol +++ b/test/instances/erc4626/MoonwellUSDC.t.sol @@ -62,6 +62,7 @@ contract MoonwellUSDCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 1e5, roundTripLpWithdrawalSharesWithBaseTolerance: 1e6, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -71,9 +72,13 @@ contract MoonwellUSDCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, // NOTE: This is high, but the vault share proceeds are always // less than the expected amount. This seems to be caused by the // lack of precision of our vault share price. For reasonable @@ -88,6 +93,9 @@ contract MoonwellUSDCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 1e12, verifyWithdrawalTolerance: 1e13 diff --git a/test/instances/erc4626/SUSDe.t.sol b/test/instances/erc4626/SUSDe.t.sol index 8426bf0f8..f027aa6d1 100644 --- a/test/instances/erc4626/SUSDe.t.sol +++ b/test/instances/erc4626/SUSDe.t.sol @@ -85,6 +85,7 @@ contract SUSDeHyperdriveTest is ERC4626HyperdriveInstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 0, roundTripLpWithdrawalSharesWithBaseTolerance: 0, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 0, @@ -94,9 +95,13 @@ contract SUSDeHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e8, roundTripLpWithdrawalSharesWithSharesTolerance: 1e8, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -106,6 +111,11 @@ contract SUSDeHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + // FIXME: Why is this so high? Shouldn't this be lower than the + // other tolerances? + roundTripPairInstantaneousWithSharesTolerance: 1e8, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/erc4626/ScrvUSD.t.sol b/test/instances/erc4626/ScrvUSD.t.sol index 0907d5dd3..dd555e417 100644 --- a/test/instances/erc4626/ScrvUSD.t.sol +++ b/test/instances/erc4626/ScrvUSD.t.sol @@ -69,6 +69,7 @@ contract scrvUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 1e5, roundTripLpWithdrawalSharesWithBaseTolerance: 1e6, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -78,9 +79,13 @@ contract scrvUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -90,6 +95,9 @@ contract scrvUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/erc4626/SnARS.t.sol b/test/instances/erc4626/SnARS.t.sol index 7a4258394..a7392cd59 100644 --- a/test/instances/erc4626/SnARS.t.sol +++ b/test/instances/erc4626/SnARS.t.sol @@ -80,6 +80,7 @@ contract SnARSHyperdriveTest is ERC4626HyperdriveInstanceTest { closeLongWithBaseTolerance: 0, closeShortWithBaseUpperBoundTolerance: 0, closeShortWithBaseTolerance: 0, + burnWithBaseTolerance: 0, roundTripLpInstantaneousWithBaseTolerance: 0, roundTripLpWithdrawalSharesWithBaseTolerance: 0, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 0, @@ -89,9 +90,13 @@ contract SnARSHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e8, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -101,6 +106,9 @@ contract SnARSHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/erc4626/StUSD.t.sol b/test/instances/erc4626/StUSD.t.sol index 1dfdf26d1..d628622ac 100644 --- a/test/instances/erc4626/StUSD.t.sol +++ b/test/instances/erc4626/StUSD.t.sol @@ -72,6 +72,7 @@ contract stUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 1e5, roundTripLpWithdrawalSharesWithBaseTolerance: 1e6, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -81,9 +82,13 @@ contract stUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -93,6 +98,9 @@ contract stUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/erc4626/sGYD.t.sol b/test/instances/erc4626/sGYD.t.sol index d52e708eb..bde9da10d 100644 --- a/test/instances/erc4626/sGYD.t.sol +++ b/test/instances/erc4626/sGYD.t.sol @@ -65,6 +65,7 @@ contract sGYDHyperdriveTest is ERC4626HyperdriveInstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 1e5, roundTripLpWithdrawalSharesWithBaseTolerance: 1e5, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -74,9 +75,13 @@ contract sGYDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -86,6 +91,9 @@ contract sGYDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/erc4626/sGYD_gnosis.t.sol b/test/instances/erc4626/sGYD_gnosis.t.sol index 59138fee3..87ed6c339 100644 --- a/test/instances/erc4626/sGYD_gnosis.t.sol +++ b/test/instances/erc4626/sGYD_gnosis.t.sol @@ -69,6 +69,7 @@ contract sGYD_gnosis_HyperdriveTest is ERC4626HyperdriveInstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 1e4, closeShortWithBaseTolerance: 1e4, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 1e5, roundTripLpWithdrawalSharesWithBaseTolerance: 1e5, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -78,9 +79,13 @@ contract sGYD_gnosis_HyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -90,6 +95,9 @@ contract sGYD_gnosis_HyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/erc4626/sUSDS.t.sol b/test/instances/erc4626/sUSDS.t.sol index 8c4c6d2a0..dd1743c73 100644 --- a/test/instances/erc4626/sUSDS.t.sol +++ b/test/instances/erc4626/sUSDS.t.sol @@ -77,6 +77,7 @@ contract sUSDSHyperdriveTest is ERC4626HyperdriveInstanceTest { closeLongWithBaseTolerance: 1e3, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 1e3, roundTripLpInstantaneousWithBaseTolerance: 1e8, roundTripLpWithdrawalSharesWithBaseTolerance: 1e8, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -86,9 +87,14 @@ contract sUSDSHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + // FIXME: Why is this higher than the other tolerances? + roundTripPairInstantaneousWithBaseTolerance: 1e8, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 1e3, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 1e3, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -98,6 +104,9 @@ contract sUSDSHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 5, verifyWithdrawalTolerance: 2 diff --git a/test/instances/erc4626/sxDai.t.sol b/test/instances/erc4626/sxDai.t.sol index 789fbf0de..1121bbd28 100644 --- a/test/instances/erc4626/sxDai.t.sol +++ b/test/instances/erc4626/sxDai.t.sol @@ -65,6 +65,7 @@ contract sxDaiHyperdriveTest is ERC4626HyperdriveInstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 1e5, roundTripLpWithdrawalSharesWithBaseTolerance: 1e5, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -74,9 +75,13 @@ contract sxDaiHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -86,6 +91,9 @@ contract sxDaiHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/ezETH/EzETHHyperdrive.t.sol b/test/instances/ezETH/EzETHHyperdrive.t.sol index 7e48447e6..f3c5ae3b5 100644 --- a/test/instances/ezETH/EzETHHyperdrive.t.sol +++ b/test/instances/ezETH/EzETHHyperdrive.t.sol @@ -89,6 +89,7 @@ contract EzETHHyperdriveTest is InstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 0, roundTripLpWithdrawalSharesWithBaseTolerance: 0, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 0, @@ -98,9 +99,13 @@ contract EzETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 1e6, closeShortWithSharesTolerance: 1e6, + burnWithSharesTolerance: 1e6, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -110,6 +115,9 @@ contract EzETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e7, roundTripShortMaturityWithSharesTolerance: 1e8, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e7, + roundTripPairMaturityWithSharesTolerance: 1e8, // The verification tolerances. verifyDepositTolerance: 300, verifyWithdrawalTolerance: 3_000 diff --git a/test/instances/ezeth-linea/EzETHLineaTest.t.sol b/test/instances/ezeth-linea/EzETHLineaTest.t.sol index 20f161667..c397bd5c2 100644 --- a/test/instances/ezeth-linea/EzETHLineaTest.t.sol +++ b/test/instances/ezeth-linea/EzETHLineaTest.t.sol @@ -75,6 +75,7 @@ contract EzETHLineaHyperdriveTest is InstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 0, roundTripLpWithdrawalSharesWithBaseTolerance: 0, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 0, @@ -84,9 +85,13 @@ contract EzETHLineaHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 100, roundTripLpWithdrawalSharesWithSharesTolerance: 1e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -96,6 +101,9 @@ contract EzETHLineaHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/lseth/LsETHHyperdrive.t.sol b/test/instances/lseth/LsETHHyperdrive.t.sol index 9bc2d64ee..468593340 100644 --- a/test/instances/lseth/LsETHHyperdrive.t.sol +++ b/test/instances/lseth/LsETHHyperdrive.t.sol @@ -74,9 +74,10 @@ contract LsETHHyperdriveTest is InstanceTest { // NOTE: Base withdrawals are disabled, so the tolerances are zero. // // The base test tolerances. - closeLongWithBaseTolerance: 20, - closeShortWithBaseUpperBoundTolerance: 10, - closeShortWithBaseTolerance: 100, + closeLongWithBaseTolerance: 0, + closeShortWithBaseUpperBoundTolerance: 0, + closeShortWithBaseTolerance: 0, + burnWithBaseTolerance: 0, roundTripLpInstantaneousWithBaseTolerance: 0, roundTripLpWithdrawalSharesWithBaseTolerance: 0, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 0, @@ -86,9 +87,13 @@ contract LsETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e3, roundTripLpWithdrawalSharesWithSharesTolerance: 1e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -98,6 +103,9 @@ contract LsETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/morpho-blue/MorphoBlue_USDe_DAI_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_USDe_DAI_Hyperdrive.t.sol index d026c22a5..eb188f5e0 100644 --- a/test/instances/morpho-blue/MorphoBlue_USDe_DAI_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_USDe_DAI_Hyperdrive.t.sol @@ -64,6 +64,7 @@ contract MorphoBlue_USDe_DAI_HyperdriveTest is closeLongWithBaseTolerance: 2, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 2, roundTripLpInstantaneousWithBaseTolerance: 1e13, roundTripLpWithdrawalSharesWithBaseTolerance: 1e13, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -73,12 +74,17 @@ contract MorphoBlue_USDe_DAI_HyperdriveTest is roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e7, roundTripShortMaturityWithBaseTolerance: 1e10, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + // FIXME: Why is this higher than the other tolerances? + roundTripPairInstantaneousWithBaseTolerance: 1e13, + roundTripPairMaturityWithBaseTolerance: 1e10, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. // // The share test tolerances. closeLongWithSharesTolerance: 0, closeShortWithSharesTolerance: 0, + burnWithSharesTolerance: 0, roundTripLpInstantaneousWithSharesTolerance: 0, roundTripLpWithdrawalSharesWithSharesTolerance: 0, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 0, @@ -88,6 +94,9 @@ contract MorphoBlue_USDe_DAI_HyperdriveTest is roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/morpho-blue/MorphoBlue_WBTC_USDC_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_WBTC_USDC_Hyperdrive.t.sol index d44f903dc..0b3e82a56 100644 --- a/test/instances/morpho-blue/MorphoBlue_WBTC_USDC_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_WBTC_USDC_Hyperdrive.t.sol @@ -63,6 +63,7 @@ contract MorphoBlue_WBTC_USDC_Hyperdrive is MorphoBlueHyperdriveInstanceTest { closeLongWithBaseTolerance: 2, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 2, roundTripLpInstantaneousWithBaseTolerance: 1e3, roundTripLpWithdrawalSharesWithBaseTolerance: 1e5, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 100, @@ -74,12 +75,16 @@ contract MorphoBlue_WBTC_USDC_Hyperdrive is MorphoBlueHyperdriveInstanceTest { // NOTE: Since the curve fee isn't zero, this check is ignored. roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 1e3, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 100, + roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. // // The share test tolerances. closeLongWithSharesTolerance: 0, closeShortWithSharesTolerance: 0, + burnWithSharesTolerance: 0, roundTripLpInstantaneousWithSharesTolerance: 0, roundTripLpWithdrawalSharesWithSharesTolerance: 0, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 0, @@ -89,6 +94,9 @@ contract MorphoBlue_WBTC_USDC_Hyperdrive is MorphoBlueHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 3 diff --git a/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol index 23e6c69d1..8452552c1 100644 --- a/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol @@ -65,6 +65,7 @@ contract MorphoBlue_cbETH_USDC_Base_HyperdriveTest is closeLongWithBaseTolerance: 2, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 2, roundTripLpInstantaneousWithBaseTolerance: 1e3, roundTripLpWithdrawalSharesWithBaseTolerance: 1e5, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 100, @@ -76,12 +77,16 @@ contract MorphoBlue_cbETH_USDC_Base_HyperdriveTest is // NOTE: Since the curve fee isn't zero, this check is ignored. roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 1e3, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 100, + roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. // // The share test tolerances. closeLongWithSharesTolerance: 0, closeShortWithSharesTolerance: 0, + burnWithSharesTolerance: 0, roundTripLpInstantaneousWithSharesTolerance: 0, roundTripLpWithdrawalSharesWithSharesTolerance: 0, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 0, @@ -91,6 +96,9 @@ contract MorphoBlue_cbETH_USDC_Base_HyperdriveTest is roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 3 diff --git a/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Mainnet_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Mainnet_Hyperdrive.t.sol index 52c0a2b91..81b5ca661 100644 --- a/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Mainnet_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Mainnet_Hyperdrive.t.sol @@ -65,6 +65,7 @@ contract MorphoBlue_cbBTC_USDC_Mainnet_HyperdriveTest is closeLongWithBaseTolerance: 2, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 2, roundTripLpInstantaneousWithBaseTolerance: 1e3, roundTripLpWithdrawalSharesWithBaseTolerance: 1e5, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 100, @@ -76,12 +77,16 @@ contract MorphoBlue_cbBTC_USDC_Mainnet_HyperdriveTest is // NOTE: Since the curve fee isn't zero, this check is ignored. roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 1e3, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 100, + roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. // // The share test tolerances. closeLongWithSharesTolerance: 0, closeShortWithSharesTolerance: 0, + burnWithSharesTolerance: 0, roundTripLpInstantaneousWithSharesTolerance: 0, roundTripLpWithdrawalSharesWithSharesTolerance: 0, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 0, @@ -91,6 +96,9 @@ contract MorphoBlue_cbBTC_USDC_Mainnet_HyperdriveTest is roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 3 diff --git a/test/instances/morpho-blue/MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol index 9c9c317bb..7d103217c 100644 --- a/test/instances/morpho-blue/MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol @@ -64,6 +64,7 @@ contract MorphoBlue_sUSDe_DAI_HyperdriveTest is closeLongWithBaseTolerance: 2, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 2, roundTripLpInstantaneousWithBaseTolerance: 1e13, roundTripLpWithdrawalSharesWithBaseTolerance: 1e13, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -73,12 +74,17 @@ contract MorphoBlue_sUSDe_DAI_HyperdriveTest is roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e8, roundTripShortMaturityWithBaseTolerance: 1e10, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + // FIXME: Why is this higher than the other tolernances? + roundTripPairInstantaneousWithBaseTolerance: 1e13, + roundTripPairMaturityWithBaseTolerance: 1e10, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. // // The share test tolerances. closeLongWithSharesTolerance: 0, closeShortWithSharesTolerance: 0, + burnWithSharesTolerance: 0, roundTripLpInstantaneousWithSharesTolerance: 0, roundTripLpWithdrawalSharesWithSharesTolerance: 0, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 0, @@ -88,6 +94,9 @@ contract MorphoBlue_sUSDe_DAI_HyperdriveTest is roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/morpho-blue/MorphoBlue_wstETH_USDA_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_wstETH_USDA_Hyperdrive.t.sol index 476f2f657..0893330a5 100644 --- a/test/instances/morpho-blue/MorphoBlue_wstETH_USDA_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_wstETH_USDA_Hyperdrive.t.sol @@ -64,6 +64,7 @@ contract MorphoBlue_wstETH_USDA_HyperdriveTest is closeLongWithBaseTolerance: 2, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 2, roundTripLpInstantaneousWithBaseTolerance: 1e13, roundTripLpWithdrawalSharesWithBaseTolerance: 1e13, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -73,12 +74,17 @@ contract MorphoBlue_wstETH_USDA_HyperdriveTest is roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e8, roundTripShortMaturityWithBaseTolerance: 1e10, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + // FIXME: Why is this higher than the other tolerances? + roundTripPairInstantaneousWithBaseTolerance: 1e13, + roundTripPairMaturityWithBaseTolerance: 1e10, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. // // The share test tolerances. closeLongWithSharesTolerance: 0, closeShortWithSharesTolerance: 0, + burnWithSharesTolerance: 0, roundTripLpInstantaneousWithSharesTolerance: 0, roundTripLpWithdrawalSharesWithSharesTolerance: 0, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 0, @@ -88,6 +94,9 @@ contract MorphoBlue_wstETH_USDA_HyperdriveTest is roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/morpho-blue/MorphoBlue_wstETH_USDC_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_wstETH_USDC_Hyperdrive.t.sol index 86db09c8a..b9747b09b 100644 --- a/test/instances/morpho-blue/MorphoBlue_wstETH_USDC_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_wstETH_USDC_Hyperdrive.t.sol @@ -65,6 +65,7 @@ contract MorphoBlue_wstETH_USDC_HyperdriveTest is closeLongWithBaseTolerance: 2, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 2, roundTripLpInstantaneousWithBaseTolerance: 1e3, roundTripLpWithdrawalSharesWithBaseTolerance: 1e5, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 100, @@ -76,12 +77,16 @@ contract MorphoBlue_wstETH_USDC_HyperdriveTest is // NOTE: Since the curve fee isn't zero, this check is ignored. roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 1e3, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 100, + roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. // // The share test tolerances. closeLongWithSharesTolerance: 0, closeShortWithSharesTolerance: 0, + burnWithSharesTolerance: 0, roundTripLpInstantaneousWithSharesTolerance: 0, roundTripLpWithdrawalSharesWithSharesTolerance: 0, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 0, @@ -91,6 +96,9 @@ contract MorphoBlue_wstETH_USDC_HyperdriveTest is roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 3 diff --git a/test/instances/reth/RETHHyperdrive.t.sol b/test/instances/reth/RETHHyperdrive.t.sol index 12149f988..ee80c0548 100644 --- a/test/instances/reth/RETHHyperdrive.t.sol +++ b/test/instances/reth/RETHHyperdrive.t.sol @@ -77,6 +77,7 @@ contract RETHHyperdriveTest is InstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 1e3, roundTripLpWithdrawalSharesWithBaseTolerance: 1e3, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -86,9 +87,13 @@ contract RETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e3, roundTripShortMaturityWithBaseTolerance: 1e3, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e3, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 2e3, roundTripLpWithdrawalSharesWithSharesTolerance: 2e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -98,6 +103,9 @@ contract RETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/rseth-linea/RsETHLineaHyperdrive.t.sol b/test/instances/rseth-linea/RsETHLineaHyperdrive.t.sol index 323cfd0c6..3549a2e2c 100644 --- a/test/instances/rseth-linea/RsETHLineaHyperdrive.t.sol +++ b/test/instances/rseth-linea/RsETHLineaHyperdrive.t.sol @@ -76,6 +76,7 @@ contract RsETHLineaHyperdriveTest is InstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 20, roundTripLpInstantaneousWithBaseTolerance: 0, roundTripLpWithdrawalSharesWithBaseTolerance: 0, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 0, @@ -85,9 +86,13 @@ contract RsETHLineaHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 100, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 100, roundTripLpInstantaneousWithSharesTolerance: 100, roundTripLpWithdrawalSharesWithSharesTolerance: 1e4, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -97,6 +102,9 @@ contract RsETHLineaHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e4, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesTolerance: 1e4, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/savings-usds-l2/SavingsUSDS_Base_Hyperdrive.t.sol b/test/instances/savings-usds-l2/SavingsUSDS_Base_Hyperdrive.t.sol index 4a32069b8..665f5b15b 100644 --- a/test/instances/savings-usds-l2/SavingsUSDS_Base_Hyperdrive.t.sol +++ b/test/instances/savings-usds-l2/SavingsUSDS_Base_Hyperdrive.t.sol @@ -63,6 +63,7 @@ contract SavingsUSDS_L2_Base_Hyperdrive is SavingsUSDSL2HyperdriveInstanceTest { closeLongWithBaseTolerance: 1e3, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 1e3, roundTripLpInstantaneousWithBaseTolerance: 1e8, roundTripLpWithdrawalSharesWithBaseTolerance: 1e8, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -72,9 +73,13 @@ contract SavingsUSDS_L2_Base_Hyperdrive is SavingsUSDSL2HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 1e3, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 1e3, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -84,6 +89,9 @@ contract SavingsUSDS_L2_Base_Hyperdrive is SavingsUSDSL2HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 5, verifyWithdrawalTolerance: 2 diff --git a/test/instances/staking-usds/StakingUSDS_Chronicle_Hyperdrive.t.sol b/test/instances/staking-usds/StakingUSDS_Chronicle_Hyperdrive.t.sol index b5cb0549d..37ef81ed1 100644 --- a/test/instances/staking-usds/StakingUSDS_Chronicle_Hyperdrive.t.sol +++ b/test/instances/staking-usds/StakingUSDS_Chronicle_Hyperdrive.t.sol @@ -57,6 +57,7 @@ contract StakingUSDS_Chronicle_Hyperdrive is StakingUSDSHyperdriveInstanceTest { closeLongWithBaseTolerance: 2, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 2, roundTripLpInstantaneousWithBaseTolerance: 1e3, roundTripLpWithdrawalSharesWithBaseTolerance: 1e7, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -68,12 +69,16 @@ contract StakingUSDS_Chronicle_Hyperdrive is StakingUSDSHyperdriveInstanceTest { // NOTE: Since the curve fee isn't zero, this check is ignored. roundTripShortInstantaneousWithBaseTolerance: 1e3, roundTripShortMaturityWithBaseTolerance: 1e3, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. // // The share test tolerances. closeLongWithSharesTolerance: 0, closeShortWithSharesTolerance: 0, + burnWithSharesTolerance: 0, roundTripLpInstantaneousWithSharesTolerance: 0, roundTripLpWithdrawalSharesWithSharesTolerance: 0, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 0, @@ -83,6 +88,9 @@ contract StakingUSDS_Chronicle_Hyperdrive is StakingUSDSHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 3 diff --git a/test/instances/staking-usds/StakingUSDS_Sky_Hyperdrive.t.sol b/test/instances/staking-usds/StakingUSDS_Sky_Hyperdrive.t.sol index 476fe85b3..402a3fdd6 100644 --- a/test/instances/staking-usds/StakingUSDS_Sky_Hyperdrive.t.sol +++ b/test/instances/staking-usds/StakingUSDS_Sky_Hyperdrive.t.sol @@ -57,6 +57,7 @@ contract StakingUSDS_Sky_Hyperdrive is StakingUSDSHyperdriveInstanceTest { closeLongWithBaseTolerance: 2, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 2, roundTripLpInstantaneousWithBaseTolerance: 1e3, roundTripLpWithdrawalSharesWithBaseTolerance: 1e7, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -68,12 +69,16 @@ contract StakingUSDS_Sky_Hyperdrive is StakingUSDSHyperdriveInstanceTest { // NOTE: Since the curve fee isn't zero, this check is ignored. roundTripShortInstantaneousWithBaseTolerance: 1e3, roundTripShortMaturityWithBaseTolerance: 1e3, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. // // The share test tolerances. closeLongWithSharesTolerance: 0, closeShortWithSharesTolerance: 0, + burnWithSharesTolerance: 0, roundTripLpInstantaneousWithSharesTolerance: 0, roundTripLpWithdrawalSharesWithSharesTolerance: 0, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 0, @@ -83,6 +88,9 @@ contract StakingUSDS_Sky_Hyperdrive is StakingUSDSHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 3 diff --git a/test/instances/steth/StETHHyperdrive.t.sol b/test/instances/steth/StETHHyperdrive.t.sol index 7dd3c4f94..d0001b370 100644 --- a/test/instances/steth/StETHHyperdrive.t.sol +++ b/test/instances/steth/StETHHyperdrive.t.sol @@ -70,9 +70,10 @@ contract StETHHyperdriveTest is InstanceTest { // NOTE: Base withdrawals are disabled, so the tolerances are zero. // // The base test tolerances. - closeLongWithBaseTolerance: 20, - closeShortWithBaseUpperBoundTolerance: 10, - closeShortWithBaseTolerance: 100, + closeLongWithBaseTolerance: 0, + closeShortWithBaseUpperBoundTolerance: 0, + closeShortWithBaseTolerance: 0, + burnWithBaseTolerance: 0, roundTripLpInstantaneousWithBaseTolerance: 0, roundTripLpWithdrawalSharesWithBaseTolerance: 0, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 0, @@ -82,9 +83,13 @@ contract StETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e5, roundTripLpWithdrawalSharesWithSharesTolerance: 1e5, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -94,6 +99,10 @@ contract StETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + // FIXME: Why is this higher than the other tolerances? + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 diff --git a/test/instances/stk-well/StkWellHyperdrive.t.sol b/test/instances/stk-well/StkWellHyperdrive.t.sol index b96352701..872c58895 100644 --- a/test/instances/stk-well/StkWellHyperdrive.t.sol +++ b/test/instances/stk-well/StkWellHyperdrive.t.sol @@ -78,6 +78,7 @@ contract StkWellHyperdriveInstanceTest is InstanceTest { closeLongWithBaseTolerance: 2, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 2, roundTripLpInstantaneousWithBaseTolerance: 1e3, roundTripLpWithdrawalSharesWithBaseTolerance: 1e7, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -87,9 +88,13 @@ contract StkWellHyperdriveInstanceTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e3, roundTripShortMaturityWithBaseTolerance: 1e3, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e3, // The share test tolerances. closeLongWithSharesTolerance: 2, closeShortWithSharesTolerance: 2, + burnWithSharesTolerance: 2, roundTripLpInstantaneousWithSharesTolerance: 1e3, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -99,6 +104,9 @@ contract StkWellHyperdriveInstanceTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 3 diff --git a/test/utils/InstanceTest.sol b/test/utils/InstanceTest.sol index 2eeab0356..2f8c50bf8 100644 --- a/test/utils/InstanceTest.sol +++ b/test/utils/InstanceTest.sol @@ -94,6 +94,12 @@ abstract contract InstanceTest is HyperdriveTest { /// @dev The equality tolerance in wei for the close short with shares /// test. uint256 closeShortWithSharesTolerance; + /// @dev The equality tolerance in wei for the burn with shares + /// test. + uint256 burnWithSharesTolerance; + /// @dev The equality tolerance in wei for the burn with base + /// test. + uint256 burnWithBaseTolerance; /// @dev The equality tolerance in wei for the instantaneous LP with /// base test. uint256 roundTripLpInstantaneousWithBaseTolerance; @@ -148,6 +154,24 @@ abstract contract InstanceTest is HyperdriveTest { /// @dev The equality tolerance in wei for the short at maturity round /// trip with shares test. uint256 roundTripShortMaturityWithSharesTolerance; + /// @dev The upper bound tolerance in wei for the instantaneous pair + /// round trip with base test. + uint256 roundTripPairInstantaneousWithBaseUpperBoundTolerance; + /// @dev The equality tolerance in wei for the instantaneous pair round + /// trip with base test. + uint256 roundTripPairInstantaneousWithBaseTolerance; + /// @dev The upper bound tolerance in wei for the instantaneous pair + /// round trip with shares test. + uint256 roundTripPairInstantaneousWithSharesUpperBoundTolerance; + /// @dev The equality tolerance in wei for the instantaneous pair round + /// trip with shares test. + uint256 roundTripPairInstantaneousWithSharesTolerance; + /// @dev The equality tolerance in wei for the pair at maturity round + /// trip with base test. + uint256 roundTripPairMaturityWithBaseTolerance; + /// @dev The equality tolerance in wei for the pair at maturity round + /// trip with shares test. + uint256 roundTripPairMaturityWithSharesTolerance; /// @dev The equality tolerance in wei for `verifyDeposit`. uint256 verifyDepositTolerance; /// @dev The equality tolerance in wei for `verifyWithdrawal`. @@ -3440,7 +3464,7 @@ abstract contract InstanceTest is HyperdriveTest { assertApproxEqAbs( baseProceeds, expectedBaseProceeds, - config.closeLongWithSharesTolerance + config.burnWithSharesTolerance ); // Ensure the withdrawal accounting is correct. @@ -3596,7 +3620,7 @@ abstract contract InstanceTest is HyperdriveTest { assertApproxEqAbs( baseProceeds, expectedBaseProceeds, - config.closeLongWithBaseTolerance + config.burnWithBaseTolerance ); // Ensure the withdrawal accounting is correct. @@ -3611,99 +3635,108 @@ abstract contract InstanceTest is HyperdriveTest { ); } - // FIXME + // FIXME: This is still failing in several places. // - // /// @dev Fuzz test that ensures that longs receive the correct payouts if - // /// they open and close instantaneously when deposits and withdrawals - // /// are made with base. - // /// @param _basePaid The fuzz parameter for the base paid. - // function test_round_trip_long_instantaneous_with_base( - // uint256 _basePaid - // ) external { - // // If base deposits aren't enabled, we skip the test. - // if (!config.enableBaseDeposits) { - // return; - // } + /// @dev Fuzz test that ensures that traders receive the correct payouts if + /// they mint and burn instantaneously when deposits and withdrawals + /// are made with base. + /// @param _basePaid The fuzz parameter for the base paid. + function test_round_trip_pair_instantaneous_with_base( + uint256 _basePaid + ) external { + // If base deposits aren't enabled, we skip the test. + if (!config.enableBaseDeposits) { + return; + } - // // Bob opens a long with base. - // _basePaid = _basePaid.normalizeToRange( - // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, - // hyperdrive.calculateMaxLong() - // ); - // (uint256 maturityTime, uint256 longAmount) = openLong(bob, _basePaid); + // Calculate the maximum amount of basePaid we can test. The limit is + // the amount of the base token that the trader has. + uint256 maxBaseAmount; + if (isBaseETH) { + maxBaseAmount = bob.balance; + } else { + maxBaseAmount = IERC20(config.baseToken).balanceOf(bob) / 10; + } - // // Get some balance information before the withdrawal. - // ( - // uint256 totalSupplyAssetsBefore, - // uint256 totalSupplySharesBefore - // ) = getSupply(); - // AccountBalances memory bobBalancesBefore = getAccountBalances(bob); - // AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( - // address(hyperdrive) - // ); + // Bob mints some bonds with base. + _basePaid = _basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + maxBaseAmount + ); + (uint256 maturityTime, uint256 bondAmount) = mint(bob, _basePaid); - // // If base withdrawals are supported, we withdraw with base. - // uint256 baseProceeds; - // if (config.enableBaseWithdraws) { - // // Bob closes his long with base as the target asset. - // baseProceeds = closeLong(bob, maturityTime, longAmount); + // Get some balance information before the withdrawal. + ( + uint256 totalSupplyAssetsBefore, + uint256 totalSupplySharesBefore + ) = getSupply(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); - // // Bob should receive less base than he paid since no time as passed. - // assertLt( - // baseProceeds, - // _basePaid + - // config.roundTripLongInstantaneousWithBaseUpperBoundTolerance - // ); - // // NOTE: If the fees aren't zero, we can't make an equality comparison. - // if (hyperdrive.getPoolConfig().fees.curve == 0) { - // assertApproxEqAbs( - // baseProceeds, - // _basePaid, - // config.roundTripLongInstantaneousWithBaseTolerance - // ); - // } - // } - // // Otherwise we withdraw with vault shares. - // else { - // // Bob closes his long with vault shares as the target asset. - // uint256 vaultSharesProceeds = closeLong( - // bob, - // maturityTime, - // longAmount, - // false - // ); - // baseProceeds = hyperdrive.convertToBase(vaultSharesProceeds); + // If base withdrawals are supported, we withdraw with base. + uint256 baseProceeds; + if (config.enableBaseWithdraws) { + // Bob burns his bonds with base as the target asset. + baseProceeds = burn(bob, maturityTime, bondAmount); - // // NOTE: We add a slight buffer since the fees are zero. - // // - // // Bob should receive less base than he paid since no time as passed. - // assertLt( - // vaultSharesProceeds, - // hyperdrive.convertToShares(_basePaid) + - // config - // .roundTripLongInstantaneousWithSharesUpperBoundTolerance - // ); - // // NOTE: If the fees aren't zero, we can't make an equality comparison. - // if (hyperdrive.getPoolConfig().fees.curve == 0) { - // assertApproxEqAbs( - // vaultSharesProceeds, - // hyperdrive.convertToShares(_basePaid), - // config.roundTripLongInstantaneousWithSharesTolerance - // ); - // } - // } + // Bob should receive less base than he paid since no time as passed. + assertLt( + baseProceeds, + _basePaid + + config.roundTripPairInstantaneousWithBaseUpperBoundTolerance + ); + // NOTE: If the fees aren't zero, we can't make an equality comparison. + if (hyperdrive.getPoolConfig().fees.governanceLP == 0) { + assertApproxEqAbs( + baseProceeds, + _basePaid, + config.roundTripPairInstantaneousWithBaseTolerance + ); + } + } + // Otherwise we withdraw with vault shares. + else { + // Bob closes his long with vault shares as the target asset. + uint256 vaultSharesProceeds = burn( + bob, + maturityTime, + bondAmount, + false + ); + baseProceeds = hyperdrive.convertToBase(vaultSharesProceeds); - // // Ensure that the withdrawal was processed as expected. - // verifyWithdrawal( - // bob, - // baseProceeds, - // config.enableBaseWithdraws, - // totalSupplyAssetsBefore, - // totalSupplySharesBefore, - // bobBalancesBefore, - // hyperdriveBalancesBefore - // ); - // } + // NOTE: We add a slight buffer since the fees are zero. + // + // Bob should receive less base than he paid since no time as passed. + assertLt( + vaultSharesProceeds, + hyperdrive.convertToShares(_basePaid) + + config + .roundTripPairInstantaneousWithSharesUpperBoundTolerance + ); + // NOTE: If the fees aren't zero, we can't make an equality comparison. + if (hyperdrive.getPoolConfig().fees.governanceLP == 0) { + assertApproxEqAbs( + vaultSharesProceeds, + hyperdrive.convertToShares(_basePaid), + config.roundTripPairInstantaneousWithSharesTolerance + ); + } + } + + // Ensure that the withdrawal was processed as expected. + verifyWithdrawal( + bob, + baseProceeds, + config.enableBaseWithdraws, + totalSupplyAssetsBefore, + totalSupplySharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } // /// @dev Fuzz test that ensures that longs receive the correct payouts if // /// they open and close instantaneously when deposits and withdrawals From 85833a7990d134724f6bdbf1ebb76a60341f0801 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 16 Jan 2025 14:11:47 -1000 Subject: [PATCH 33/45] Uncommented and fixed another test --- test/instances/aave/AaveHyperdrive.t.sol | 23 +- test/instances/aave/AaveL2Hyperdrive.t.sol | 32 +- .../AerodromeLp_AERO_USDC_Hyperdrive.t.sol | 2 + test/instances/chainlink/CbETHBase.t.sol | 2 + .../chainlink/WstETHGnosisChain.t.sol | 2 + test/instances/corn/Corn_LBTC_Hyperdrive.sol | 2 + .../instances/corn/Corn_sDAI_Hyperdrive.t.sol | 2 + test/instances/eeth/EETHHyperdrive.t.sol | 2 + test/instances/erc4626/MoonwellETH.t.sol | 2 + test/instances/erc4626/MoonwellEURC.t.sol | 2 + test/instances/erc4626/MoonwellUSDC.t.sol | 2 + test/instances/erc4626/SUSDe.t.sol | 2 + test/instances/erc4626/ScrvUSD.t.sol | 2 + test/instances/erc4626/SnARS.t.sol | 2 + test/instances/erc4626/StUSD.t.sol | 2 + test/instances/erc4626/sGYD.t.sol | 2 + test/instances/erc4626/sGYD_gnosis.t.sol | 2 + test/instances/erc4626/sUSDS.t.sol | 5 +- test/instances/erc4626/sxDai.t.sol | 5 +- test/instances/ezETH/EzETHHyperdrive.t.sol | 2 + .../ezeth-linea/EzETHLineaTest.t.sol | 2 + test/instances/lseth/LsETHHyperdrive.t.sol | 2 + .../MorphoBlue_USDe_DAI_Hyperdrive.t.sol | 2 + .../MorphoBlue_WBTC_USDC_Hyperdrive.t.sol | 2 + ...orphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol | 2 + ...hoBlue_cbETH_USDC_Mainnet_Hyperdrive.t.sol | 2 + .../MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol | 2 + .../MorphoBlue_wstETH_USDA_Hyperdrive.t.sol | 2 + .../MorphoBlue_wstETH_USDC_Hyperdrive.t.sol | 2 + test/instances/reth/RETHHyperdrive.t.sol | 4 +- .../rseth-linea/RsETHLineaHyperdrive.t.sol | 2 + .../SavingsUSDS_Base_Hyperdrive.t.sol | 2 + .../StakingUSDS_Chronicle_Hyperdrive.t.sol | 2 + .../StakingUSDS_Sky_Hyperdrive.t.sol | 2 + test/instances/steth/StETHHyperdrive.t.sol | 2 + .../stk-well/StkWellHyperdrive.t.sol | 2 + test/utils/InstanceTest.sol | 417 ++++++++++-------- 37 files changed, 359 insertions(+), 189 deletions(-) diff --git a/test/instances/aave/AaveHyperdrive.t.sol b/test/instances/aave/AaveHyperdrive.t.sol index b158239b3..c1ea3be28 100644 --- a/test/instances/aave/AaveHyperdrive.t.sol +++ b/test/instances/aave/AaveHyperdrive.t.sol @@ -22,6 +22,7 @@ import { Lib } from "../../utils/Lib.sol"; contract AaveHyperdriveTest is InstanceTest { using FixedPointMath for uint256; + using HyperdriveUtils for *; using Lib for *; using stdStorage for StdStorage; @@ -87,7 +88,8 @@ contract AaveHyperdriveTest is InstanceTest { roundTripShortMaturityWithBaseTolerance: 1e5, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e5, - roundTripPairMaturityWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, @@ -101,9 +103,10 @@ contract AaveHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, - roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, - roundTripPairInstantaneousWithSharesTolerance: 0, - roundTripPairMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 @@ -232,6 +235,9 @@ contract AaveHyperdriveTest is InstanceTest { uint256 timeDelta, int256 variableRate ) internal override { + // Get the base balance before updating the time. + uint256 baseBalance = WETH.balanceOf(address(AWETH)); + // Get the normalized income prior to updating the time. uint256 reserveNormalizedIncome = POOL.getReserveNormalizedIncome( address(WETH) @@ -277,5 +283,14 @@ contract AaveHyperdriveTest is InstanceTest { data.currentStableBorrowRate ) ); + + // Mint more of the base token to the Aave pool to ensure that it + // remains solvent. + (baseBalance, ) = baseBalance.calculateInterest( + variableRate, + timeDelta + ); + bytes32 balanceLocation = keccak256(abi.encode(address(AWETH), 3)); + vm.store(address(WETH), balanceLocation, bytes32(baseBalance)); } } diff --git a/test/instances/aave/AaveL2Hyperdrive.t.sol b/test/instances/aave/AaveL2Hyperdrive.t.sol index 2b014db73..2cdb1ba05 100644 --- a/test/instances/aave/AaveL2Hyperdrive.t.sol +++ b/test/instances/aave/AaveL2Hyperdrive.t.sol @@ -23,6 +23,7 @@ import { Lib } from "test/utils/Lib.sol"; contract AaveL2HyperdriveTest is InstanceTest { using FixedPointMath for uint256; + using HyperdriveUtils for *; using Lib for *; using stdStorage for StdStorage; @@ -97,6 +98,7 @@ contract AaveL2HyperdriveTest is InstanceTest { roundTripShortMaturityWithBaseTolerance: 1e5, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -113,6 +115,7 @@ contract AaveL2HyperdriveTest is InstanceTest { roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, @@ -234,6 +237,9 @@ contract AaveL2HyperdriveTest is InstanceTest { uint256 timeDelta, int256 variableRate ) internal override { + // Get the base balance before updating the time. + uint256 baseBalance = WETH.balanceOf(address(AWETH)); + // Get the normalized income prior to updating the time. uint256 reserveNormalizedIncome = POOL.getReserveNormalizedIncome( address(WETH) @@ -248,11 +254,16 @@ contract AaveL2HyperdriveTest is InstanceTest { // variable rate plus one. We also need to increase the // `lastUpdatedTimestamp` to avoid accruing interest when deposits or // withdrawals are processed. - (uint256 totalAmount, ) = HyperdriveUtils.calculateInterest( - reserveNormalizedIncome, - variableRate, - timeDelta - ); + uint256 normalizedTime = timeDelta.divDown(365 days); + reserveNormalizedIncome = variableRate >= 0 + ? reserveNormalizedIncome + + reserveNormalizedIncome.mulDown(uint256(variableRate)).mulDown( + normalizedTime + ) + : reserveNormalizedIncome - + reserveNormalizedIncome.mulDown(uint256(-variableRate)).mulDown( + normalizedTime + ); bytes32 reserveDataLocation = keccak256(abi.encode(address(WETH), 52)); DataTypes.ReserveDataLegacy memory data = POOL.getReserveData( address(WETH) @@ -262,7 +273,7 @@ contract AaveL2HyperdriveTest is InstanceTest { bytes32(uint256(reserveDataLocation) + 1), bytes32( (uint256(data.currentLiquidityRate) << 128) | - uint256(totalAmount) + uint256(reserveNormalizedIncome) ) ); vm.store( @@ -274,5 +285,14 @@ contract AaveL2HyperdriveTest is InstanceTest { data.currentStableBorrowRate ) ); + + // Mint more of the base token to the Aave pool to ensure that it + // remains solvent. + (baseBalance, ) = baseBalance.calculateInterest( + variableRate, + timeDelta + ); + bytes32 balanceLocation = keccak256(abi.encode(address(AWETH), 5)); + vm.store(address(WETH), balanceLocation, bytes32(baseBalance)); } } diff --git a/test/instances/aerodrome/AerodromeLp_AERO_USDC_Hyperdrive.t.sol b/test/instances/aerodrome/AerodromeLp_AERO_USDC_Hyperdrive.t.sol index 75eb09286..208343de6 100644 --- a/test/instances/aerodrome/AerodromeLp_AERO_USDC_Hyperdrive.t.sol +++ b/test/instances/aerodrome/AerodromeLp_AERO_USDC_Hyperdrive.t.sol @@ -71,6 +71,7 @@ contract AerodromeLp_AERO_USDC_Hyperdrive is AerodromeLpHyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 0, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 100, roundTripPairInstantaneousWithBaseTolerance: 10, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, roundTripPairMaturityWithBaseTolerance: 0, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. @@ -90,6 +91,7 @@ contract AerodromeLp_AERO_USDC_Hyperdrive is AerodromeLpHyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 0, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 0, roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/chainlink/CbETHBase.t.sol b/test/instances/chainlink/CbETHBase.t.sol index 5dd6b0aaf..9c0b5634c 100644 --- a/test/instances/chainlink/CbETHBase.t.sol +++ b/test/instances/chainlink/CbETHBase.t.sol @@ -70,6 +70,7 @@ contract CbETHBaseTest is ChainlinkHyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 0, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -86,6 +87,7 @@ contract CbETHBaseTest is ChainlinkHyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 1e3, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/chainlink/WstETHGnosisChain.t.sol b/test/instances/chainlink/WstETHGnosisChain.t.sol index b74c0f774..734d7f98b 100644 --- a/test/instances/chainlink/WstETHGnosisChain.t.sol +++ b/test/instances/chainlink/WstETHGnosisChain.t.sol @@ -71,6 +71,7 @@ contract WstETHGnosisChainTest is ChainlinkHyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 0, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -87,6 +88,7 @@ contract WstETHGnosisChainTest is ChainlinkHyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 1e3, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/corn/Corn_LBTC_Hyperdrive.sol b/test/instances/corn/Corn_LBTC_Hyperdrive.sol index 62679a41c..145754c59 100644 --- a/test/instances/corn/Corn_LBTC_Hyperdrive.sol +++ b/test/instances/corn/Corn_LBTC_Hyperdrive.sol @@ -71,6 +71,7 @@ contract Corn_LBTC_Hyperdrive is CornHyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e3, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 100, roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. @@ -90,6 +91,7 @@ contract Corn_LBTC_Hyperdrive is CornHyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 0, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 0, roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/corn/Corn_sDAI_Hyperdrive.t.sol b/test/instances/corn/Corn_sDAI_Hyperdrive.t.sol index b15556e5d..e5867a8a7 100644 --- a/test/instances/corn/Corn_sDAI_Hyperdrive.t.sol +++ b/test/instances/corn/Corn_sDAI_Hyperdrive.t.sol @@ -71,6 +71,7 @@ contract Corn_sDAI_Hyperdrive is CornHyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e3, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. @@ -90,6 +91,7 @@ contract Corn_sDAI_Hyperdrive is CornHyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 0, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 0, roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/eeth/EETHHyperdrive.t.sol b/test/instances/eeth/EETHHyperdrive.t.sol index 5ad76df80..5087e8a17 100644 --- a/test/instances/eeth/EETHHyperdrive.t.sol +++ b/test/instances/eeth/EETHHyperdrive.t.sol @@ -91,6 +91,7 @@ contract EETHHyperdriveTest is InstanceTest { roundTripShortMaturityWithBaseTolerance: 0, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -107,6 +108,7 @@ contract EETHHyperdriveTest is InstanceTest { roundTripShortMaturityWithSharesTolerance: 1e4, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e4, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/erc4626/MoonwellETH.t.sol b/test/instances/erc4626/MoonwellETH.t.sol index 4da2d659b..229d48fbf 100644 --- a/test/instances/erc4626/MoonwellETH.t.sol +++ b/test/instances/erc4626/MoonwellETH.t.sol @@ -67,6 +67,7 @@ contract MoonwellETHHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e5, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -83,6 +84,7 @@ contract MoonwellETHHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/erc4626/MoonwellEURC.t.sol b/test/instances/erc4626/MoonwellEURC.t.sol index 66d33dd3f..b687fb1bf 100644 --- a/test/instances/erc4626/MoonwellEURC.t.sol +++ b/test/instances/erc4626/MoonwellEURC.t.sol @@ -74,6 +74,7 @@ contract MoonwellEURCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e5, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -90,6 +91,7 @@ contract MoonwellEURCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 1e12, diff --git a/test/instances/erc4626/MoonwellUSDC.t.sol b/test/instances/erc4626/MoonwellUSDC.t.sol index d7d7ac9b3..53852769e 100644 --- a/test/instances/erc4626/MoonwellUSDC.t.sol +++ b/test/instances/erc4626/MoonwellUSDC.t.sol @@ -74,6 +74,7 @@ contract MoonwellUSDCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e5, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -95,6 +96,7 @@ contract MoonwellUSDCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 1e12, diff --git a/test/instances/erc4626/SUSDe.t.sol b/test/instances/erc4626/SUSDe.t.sol index f027aa6d1..1a57cc906 100644 --- a/test/instances/erc4626/SUSDe.t.sol +++ b/test/instances/erc4626/SUSDe.t.sol @@ -97,6 +97,7 @@ contract SUSDeHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 0, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -115,6 +116,7 @@ contract SUSDeHyperdriveTest is ERC4626HyperdriveInstanceTest { // FIXME: Why is this so high? Shouldn't this be lower than the // other tolerances? roundTripPairInstantaneousWithSharesTolerance: 1e8, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/erc4626/ScrvUSD.t.sol b/test/instances/erc4626/ScrvUSD.t.sol index dd555e417..3a7c62533 100644 --- a/test/instances/erc4626/ScrvUSD.t.sol +++ b/test/instances/erc4626/ScrvUSD.t.sol @@ -81,6 +81,7 @@ contract scrvUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e5, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -97,6 +98,7 @@ contract scrvUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/erc4626/SnARS.t.sol b/test/instances/erc4626/SnARS.t.sol index a7392cd59..edede9e6d 100644 --- a/test/instances/erc4626/SnARS.t.sol +++ b/test/instances/erc4626/SnARS.t.sol @@ -92,6 +92,7 @@ contract SnARSHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 0, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -108,6 +109,7 @@ contract SnARSHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/erc4626/StUSD.t.sol b/test/instances/erc4626/StUSD.t.sol index d628622ac..f8285ca8b 100644 --- a/test/instances/erc4626/StUSD.t.sol +++ b/test/instances/erc4626/StUSD.t.sol @@ -84,6 +84,7 @@ contract stUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e5, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -100,6 +101,7 @@ contract stUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/erc4626/sGYD.t.sol b/test/instances/erc4626/sGYD.t.sol index bde9da10d..69eb41197 100644 --- a/test/instances/erc4626/sGYD.t.sol +++ b/test/instances/erc4626/sGYD.t.sol @@ -77,6 +77,7 @@ contract sGYDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e5, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -93,6 +94,7 @@ contract sGYDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/erc4626/sGYD_gnosis.t.sol b/test/instances/erc4626/sGYD_gnosis.t.sol index 87ed6c339..ac5a75b02 100644 --- a/test/instances/erc4626/sGYD_gnosis.t.sol +++ b/test/instances/erc4626/sGYD_gnosis.t.sol @@ -81,6 +81,7 @@ contract sGYD_gnosis_HyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e5, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -97,6 +98,7 @@ contract sGYD_gnosis_HyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/erc4626/sUSDS.t.sol b/test/instances/erc4626/sUSDS.t.sol index dd1743c73..2fec8bb82 100644 --- a/test/instances/erc4626/sUSDS.t.sol +++ b/test/instances/erc4626/sUSDS.t.sol @@ -90,6 +90,7 @@ contract sUSDSHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, // FIXME: Why is this higher than the other tolerances? roundTripPairInstantaneousWithBaseTolerance: 1e8, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 1e3, @@ -105,7 +106,9 @@ contract sUSDSHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, - roundTripPairInstantaneousWithSharesTolerance: 1e5, + // FIXME: Why is this higher than the other tolerances? + roundTripPairInstantaneousWithSharesTolerance: 1e6, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 5, diff --git a/test/instances/erc4626/sxDai.t.sol b/test/instances/erc4626/sxDai.t.sol index 1121bbd28..caf6a8ae3 100644 --- a/test/instances/erc4626/sxDai.t.sol +++ b/test/instances/erc4626/sxDai.t.sol @@ -77,6 +77,7 @@ contract sxDaiHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e5, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -92,7 +93,9 @@ contract sxDaiHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, - roundTripPairInstantaneousWithSharesTolerance: 1e5, + // FIXME: Why is this higher? + roundTripPairInstantaneousWithSharesTolerance: 1e6, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/ezETH/EzETHHyperdrive.t.sol b/test/instances/ezETH/EzETHHyperdrive.t.sol index f3c5ae3b5..627da2a30 100644 --- a/test/instances/ezETH/EzETHHyperdrive.t.sol +++ b/test/instances/ezETH/EzETHHyperdrive.t.sol @@ -101,6 +101,7 @@ contract EzETHHyperdriveTest is InstanceTest { roundTripShortMaturityWithBaseTolerance: 0, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 1e6, @@ -117,6 +118,7 @@ contract EzETHHyperdriveTest is InstanceTest { roundTripShortMaturityWithSharesTolerance: 1e8, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e7, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e8, // The verification tolerances. verifyDepositTolerance: 300, diff --git a/test/instances/ezeth-linea/EzETHLineaTest.t.sol b/test/instances/ezeth-linea/EzETHLineaTest.t.sol index c397bd5c2..c17c36763 100644 --- a/test/instances/ezeth-linea/EzETHLineaTest.t.sol +++ b/test/instances/ezeth-linea/EzETHLineaTest.t.sol @@ -87,6 +87,7 @@ contract EzETHLineaHyperdriveTest is InstanceTest { roundTripShortMaturityWithBaseTolerance: 0, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -103,6 +104,7 @@ contract EzETHLineaHyperdriveTest is InstanceTest { roundTripShortMaturityWithSharesTolerance: 1e3, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/lseth/LsETHHyperdrive.t.sol b/test/instances/lseth/LsETHHyperdrive.t.sol index 468593340..a44a99dae 100644 --- a/test/instances/lseth/LsETHHyperdrive.t.sol +++ b/test/instances/lseth/LsETHHyperdrive.t.sol @@ -89,6 +89,7 @@ contract LsETHHyperdriveTest is InstanceTest { roundTripShortMaturityWithBaseTolerance: 0, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -105,6 +106,7 @@ contract LsETHHyperdriveTest is InstanceTest { roundTripShortMaturityWithSharesTolerance: 1e3, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/morpho-blue/MorphoBlue_USDe_DAI_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_USDe_DAI_Hyperdrive.t.sol index eb188f5e0..f6e5c54f7 100644 --- a/test/instances/morpho-blue/MorphoBlue_USDe_DAI_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_USDe_DAI_Hyperdrive.t.sol @@ -77,6 +77,7 @@ contract MorphoBlue_USDe_DAI_HyperdriveTest is roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, // FIXME: Why is this higher than the other tolerances? roundTripPairInstantaneousWithBaseTolerance: 1e13, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e10, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. @@ -96,6 +97,7 @@ contract MorphoBlue_USDe_DAI_HyperdriveTest is roundTripShortMaturityWithSharesTolerance: 0, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 0, roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/morpho-blue/MorphoBlue_WBTC_USDC_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_WBTC_USDC_Hyperdrive.t.sol index 0b3e82a56..8d3962762 100644 --- a/test/instances/morpho-blue/MorphoBlue_WBTC_USDC_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_WBTC_USDC_Hyperdrive.t.sol @@ -77,6 +77,7 @@ contract MorphoBlue_WBTC_USDC_Hyperdrive is MorphoBlueHyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e3, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 100, roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. @@ -96,6 +97,7 @@ contract MorphoBlue_WBTC_USDC_Hyperdrive is MorphoBlueHyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 0, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 0, roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol index 8452552c1..1bf55da41 100644 --- a/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol @@ -79,6 +79,7 @@ contract MorphoBlue_cbETH_USDC_Base_HyperdriveTest is roundTripShortMaturityWithBaseTolerance: 1e3, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 100, roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. @@ -98,6 +99,7 @@ contract MorphoBlue_cbETH_USDC_Base_HyperdriveTest is roundTripShortMaturityWithSharesTolerance: 0, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 0, roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Mainnet_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Mainnet_Hyperdrive.t.sol index 81b5ca661..3f4f69901 100644 --- a/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Mainnet_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Mainnet_Hyperdrive.t.sol @@ -79,6 +79,7 @@ contract MorphoBlue_cbBTC_USDC_Mainnet_HyperdriveTest is roundTripShortMaturityWithBaseTolerance: 1e3, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 100, roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. @@ -98,6 +99,7 @@ contract MorphoBlue_cbBTC_USDC_Mainnet_HyperdriveTest is roundTripShortMaturityWithSharesTolerance: 0, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 0, roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/morpho-blue/MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol index 7d103217c..93ec0c86c 100644 --- a/test/instances/morpho-blue/MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol @@ -77,6 +77,7 @@ contract MorphoBlue_sUSDe_DAI_HyperdriveTest is roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, // FIXME: Why is this higher than the other tolernances? roundTripPairInstantaneousWithBaseTolerance: 1e13, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e10, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. @@ -96,6 +97,7 @@ contract MorphoBlue_sUSDe_DAI_HyperdriveTest is roundTripShortMaturityWithSharesTolerance: 0, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 0, roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/morpho-blue/MorphoBlue_wstETH_USDA_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_wstETH_USDA_Hyperdrive.t.sol index 0893330a5..287096c61 100644 --- a/test/instances/morpho-blue/MorphoBlue_wstETH_USDA_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_wstETH_USDA_Hyperdrive.t.sol @@ -77,6 +77,7 @@ contract MorphoBlue_wstETH_USDA_HyperdriveTest is roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, // FIXME: Why is this higher than the other tolerances? roundTripPairInstantaneousWithBaseTolerance: 1e13, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e10, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. @@ -96,6 +97,7 @@ contract MorphoBlue_wstETH_USDA_HyperdriveTest is roundTripShortMaturityWithSharesTolerance: 0, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 0, roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/morpho-blue/MorphoBlue_wstETH_USDC_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_wstETH_USDC_Hyperdrive.t.sol index b9747b09b..b36e6e230 100644 --- a/test/instances/morpho-blue/MorphoBlue_wstETH_USDC_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_wstETH_USDC_Hyperdrive.t.sol @@ -79,6 +79,7 @@ contract MorphoBlue_wstETH_USDC_HyperdriveTest is roundTripShortMaturityWithBaseTolerance: 1e3, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 100, roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. @@ -98,6 +99,7 @@ contract MorphoBlue_wstETH_USDC_HyperdriveTest is roundTripShortMaturityWithSharesTolerance: 0, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 0, roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/reth/RETHHyperdrive.t.sol b/test/instances/reth/RETHHyperdrive.t.sol index ee80c0548..928ce9f1b 100644 --- a/test/instances/reth/RETHHyperdrive.t.sol +++ b/test/instances/reth/RETHHyperdrive.t.sol @@ -89,6 +89,7 @@ contract RETHHyperdriveTest is InstanceTest { roundTripShortMaturityWithBaseTolerance: 1e3, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e3, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -104,7 +105,8 @@ contract RETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, - roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e4, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/rseth-linea/RsETHLineaHyperdrive.t.sol b/test/instances/rseth-linea/RsETHLineaHyperdrive.t.sol index 3549a2e2c..41cfbee6e 100644 --- a/test/instances/rseth-linea/RsETHLineaHyperdrive.t.sol +++ b/test/instances/rseth-linea/RsETHLineaHyperdrive.t.sol @@ -88,6 +88,7 @@ contract RsETHLineaHyperdriveTest is InstanceTest { roundTripShortMaturityWithBaseTolerance: 0, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 100, @@ -104,6 +105,7 @@ contract RsETHLineaHyperdriveTest is InstanceTest { roundTripShortMaturityWithSharesTolerance: 1e4, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, roundTripPairMaturityWithSharesTolerance: 1e4, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/savings-usds-l2/SavingsUSDS_Base_Hyperdrive.t.sol b/test/instances/savings-usds-l2/SavingsUSDS_Base_Hyperdrive.t.sol index 665f5b15b..94017b3b0 100644 --- a/test/instances/savings-usds-l2/SavingsUSDS_Base_Hyperdrive.t.sol +++ b/test/instances/savings-usds-l2/SavingsUSDS_Base_Hyperdrive.t.sol @@ -75,6 +75,7 @@ contract SavingsUSDS_L2_Base_Hyperdrive is SavingsUSDSL2HyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e5, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 1e3, @@ -91,6 +92,7 @@ contract SavingsUSDS_L2_Base_Hyperdrive is SavingsUSDSL2HyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 5, diff --git a/test/instances/staking-usds/StakingUSDS_Chronicle_Hyperdrive.t.sol b/test/instances/staking-usds/StakingUSDS_Chronicle_Hyperdrive.t.sol index 37ef81ed1..97fd26234 100644 --- a/test/instances/staking-usds/StakingUSDS_Chronicle_Hyperdrive.t.sol +++ b/test/instances/staking-usds/StakingUSDS_Chronicle_Hyperdrive.t.sol @@ -71,6 +71,7 @@ contract StakingUSDS_Chronicle_Hyperdrive is StakingUSDSHyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e3, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. @@ -90,6 +91,7 @@ contract StakingUSDS_Chronicle_Hyperdrive is StakingUSDSHyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 0, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 0, roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/staking-usds/StakingUSDS_Sky_Hyperdrive.t.sol b/test/instances/staking-usds/StakingUSDS_Sky_Hyperdrive.t.sol index 402a3fdd6..4ca77cc66 100644 --- a/test/instances/staking-usds/StakingUSDS_Sky_Hyperdrive.t.sol +++ b/test/instances/staking-usds/StakingUSDS_Sky_Hyperdrive.t.sol @@ -71,6 +71,7 @@ contract StakingUSDS_Sky_Hyperdrive is StakingUSDSHyperdriveInstanceTest { roundTripShortMaturityWithBaseTolerance: 1e3, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, roundTripPairMaturityWithBaseTolerance: 1e3, // NOTE: Share deposits and withdrawals are disabled, so these are // 0. @@ -90,6 +91,7 @@ contract StakingUSDS_Sky_Hyperdrive is StakingUSDSHyperdriveInstanceTest { roundTripShortMaturityWithSharesTolerance: 0, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 0, roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/steth/StETHHyperdrive.t.sol b/test/instances/steth/StETHHyperdrive.t.sol index d0001b370..56781fb47 100644 --- a/test/instances/steth/StETHHyperdrive.t.sol +++ b/test/instances/steth/StETHHyperdrive.t.sol @@ -85,6 +85,7 @@ contract StETHHyperdriveTest is InstanceTest { roundTripShortMaturityWithBaseTolerance: 0, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, @@ -102,6 +103,7 @@ contract StETHHyperdriveTest is InstanceTest { roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, // FIXME: Why is this higher than the other tolerances? roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/instances/stk-well/StkWellHyperdrive.t.sol b/test/instances/stk-well/StkWellHyperdrive.t.sol index 872c58895..bd85772b1 100644 --- a/test/instances/stk-well/StkWellHyperdrive.t.sol +++ b/test/instances/stk-well/StkWellHyperdrive.t.sol @@ -90,6 +90,7 @@ contract StkWellHyperdriveInstanceTest is InstanceTest { roundTripShortMaturityWithBaseTolerance: 1e3, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, roundTripPairMaturityWithBaseTolerance: 1e3, // The share test tolerances. closeLongWithSharesTolerance: 2, @@ -106,6 +107,7 @@ contract StkWellHyperdriveInstanceTest is InstanceTest { roundTripShortMaturityWithSharesTolerance: 1e3, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, diff --git a/test/utils/InstanceTest.sol b/test/utils/InstanceTest.sol index 2f8c50bf8..e0b12ae3c 100644 --- a/test/utils/InstanceTest.sol +++ b/test/utils/InstanceTest.sol @@ -166,9 +166,15 @@ abstract contract InstanceTest is HyperdriveTest { /// @dev The equality tolerance in wei for the instantaneous pair round /// trip with shares test. uint256 roundTripPairInstantaneousWithSharesTolerance; + /// @dev The upper bound tolerance in wei for the pair at maturity round + /// trip with base test. + uint256 roundTripPairMaturityWithBaseUpperBoundTolerance; /// @dev The equality tolerance in wei for the pair at maturity round /// trip with base test. uint256 roundTripPairMaturityWithBaseTolerance; + /// @dev The upper bound tolerance in wei for the pair at maturity round + /// trip with shares test. + uint256 roundTripPairMaturityWithSharesUpperBoundTolerance; /// @dev The equality tolerance in wei for the pair at maturity round /// trip with shares test. uint256 roundTripPairMaturityWithSharesTolerance; @@ -3635,8 +3641,6 @@ abstract contract InstanceTest is HyperdriveTest { ); } - // FIXME: This is still failing in several places. - // /// @dev Fuzz test that ensures that traders receive the correct payouts if /// they mint and burn instantaneously when deposits and withdrawals /// are made with base. @@ -3738,194 +3742,255 @@ abstract contract InstanceTest is HyperdriveTest { ); } - // /// @dev Fuzz test that ensures that longs receive the correct payouts if - // /// they open and close instantaneously when deposits and withdrawals - // /// are made with vault shares. - // /// @param _vaultSharesPaid The fuzz parameter for the vault shares paid. - // function test_round_trip_long_instantaneous_with_shares( - // uint256 _vaultSharesPaid - // ) external { - // // If share deposits aren't enabled, we skip the test. - // if (!config.enableShareDeposits) { - // return; - // } + /// @dev Fuzz test that ensures that traders receive the correct payouts if + /// they mint and burn instantaneously when deposits and withdrawals + /// are made with vault shares. + /// @param _vaultSharesPaid The fuzz parameter for the vault shares paid. + function test_round_trip_pair_instantaneous_with_shares( + uint256 _vaultSharesPaid + ) external { + // If share deposits aren't enabled, we skip the test. + if (!config.enableShareDeposits) { + return; + } - // // Bob opens a long with vault shares. - // _vaultSharesPaid = hyperdrive.convertToShares( - // _vaultSharesPaid.normalizeToRange( - // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, - // hyperdrive.calculateMaxLong() - // ) - // ); - // (uint256 maturityTime, uint256 longAmount) = openLong( - // bob, - // _vaultSharesPaid, - // false - // ); + // Calculate the maximum amount of sharesPaid we can test. The limit + // is the amount of the vault shares token that the trader has. + uint256 maxSharesAmount = IERC20(config.vaultSharesToken).balanceOf( + bob + ) / 10; - // // Get some balance information before the withdrawal. - // ( - // uint256 totalSupplyAssetsBefore, - // uint256 totalSupplySharesBefore - // ) = getSupply(); - // AccountBalances memory bobBalancesBefore = getAccountBalances(bob); - // AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( - // address(hyperdrive) - // ); + // We normalize the basePaid variable within a valid range the market + // can support. + if (config.isRebasing) { + _vaultSharesPaid = _vaultSharesPaid.normalizeToRange( + convertToShares( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount + ), + convertToShares(maxSharesAmount) + ); + } else { + _vaultSharesPaid = _vaultSharesPaid.normalizeToRange( + convertToShares( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount + ), + maxSharesAmount + ); + } - // // If vault share withdrawals are supported, we withdraw with vault - // // shares. - // uint256 baseProceeds; - // if (config.enableShareWithdraws) { - // // Bob closes his long with vault shares as the target asset. - // uint256 vaultSharesProceeds = closeLong( - // bob, - // maturityTime, - // longAmount, - // false - // ); - // baseProceeds = hyperdrive.convertToBase(vaultSharesProceeds); + // Bob mints some bonds with vault shares. + (uint256 maturityTime, uint256 bondAmount) = mint( + bob, + _vaultSharesPaid, + false + ); - // // Bob should receive less base than he paid since no time as passed. - // assertLt( - // vaultSharesProceeds, - // _vaultSharesPaid + - // config - // .roundTripLongInstantaneousWithSharesUpperBoundTolerance - // ); - // // NOTE: If the fees aren't zero, we can't make an equality comparison. - // if (hyperdrive.getPoolConfig().fees.curve == 0) { - // assertApproxEqAbs( - // vaultSharesProceeds, - // _vaultSharesPaid, - // config.roundTripLongInstantaneousWithSharesTolerance - // ); - // } - // } - // // Otherwise we withdraw with base. - // else { - // // Bob closes his long with base as the target asset. - // baseProceeds = closeLong(bob, maturityTime, longAmount); + // Get some balance information before the withdrawal. + ( + uint256 totalSupplyAssetsBefore, + uint256 totalSupplySharesBefore + ) = getSupply(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); - // // Bob should receive less base than he paid since no time as passed. - // assertLt( - // baseProceeds, - // hyperdrive.convertToBase(_vaultSharesPaid) + - // config.roundTripLongInstantaneousWithBaseUpperBoundTolerance - // ); - // // NOTE: If the fees aren't zero, we can't make an equality comparison. - // if (hyperdrive.getPoolConfig().fees.curve == 0) { - // assertApproxEqAbs( - // baseProceeds, - // hyperdrive.convertToBase(_vaultSharesPaid), - // config.roundTripLongInstantaneousWithBaseTolerance - // ); - // } - // } + // If vault share withdrawals are supported, we withdraw with vault + // shares. + uint256 baseProceeds; + if (config.enableShareWithdraws) { + // Bob burns his bonds with vault shares as the target asset. + uint256 vaultSharesProceeds = burn( + bob, + maturityTime, + bondAmount, + false + ); + baseProceeds = hyperdrive.convertToBase(vaultSharesProceeds); - // // Ensure that the withdrawal was processed as expected. - // verifyWithdrawal( - // bob, - // baseProceeds, - // !config.enableShareWithdraws, - // totalSupplyAssetsBefore, - // totalSupplySharesBefore, - // bobBalancesBefore, - // hyperdriveBalancesBefore - // ); - // } + // Bob should receive less base than he paid since no time as passed. + assertLt( + vaultSharesProceeds, + _vaultSharesPaid + + config + .roundTripPairInstantaneousWithSharesUpperBoundTolerance + ); + // NOTE: If the fees aren't zero, we can't make an equality comparison. + if (hyperdrive.getPoolConfig().fees.curve == 0) { + assertApproxEqAbs( + vaultSharesProceeds, + _vaultSharesPaid, + config.roundTripPairInstantaneousWithSharesTolerance + ); + } + } + // Otherwise we withdraw with base. + else { + // Bob closes his long with base as the target asset. + baseProceeds = burn(bob, maturityTime, bondAmount); - // /// @dev Fuzz test that ensures that shorts receive the correct payouts at - // /// maturity when deposits and withdrawals are made with base. - // /// @param _basePaid The fuzz parameter for the base paid. - // /// @param _variableRate The fuzz parameter for the variable rate. - // function test_round_trip_long_maturity_with_base( - // uint256 _basePaid, - // uint256 _variableRate - // ) external { - // // If base deposits aren't enabled, we skip the test. - // if (!config.enableBaseDeposits) { - // return; - // } + // Bob should receive less base than he paid since no time as passed. + assertLt( + baseProceeds, + hyperdrive.convertToBase(_vaultSharesPaid) + + config.roundTripPairInstantaneousWithBaseUpperBoundTolerance + ); + // NOTE: If the fees aren't zero, we can't make an equality comparison. + if (hyperdrive.getPoolConfig().fees.curve == 0) { + assertApproxEqAbs( + baseProceeds, + hyperdrive.convertToBase(_vaultSharesPaid), + config.roundTripPairInstantaneousWithBaseTolerance + ); + } + } - // // Bob opens a long with base. - // _basePaid = _basePaid.normalizeToRange( - // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, - // hyperdrive.calculateMaxLong() - // ); - // (uint256 maturityTime, uint256 longAmount) = openLong(bob, _basePaid); + // Ensure that the withdrawal was processed as expected. + verifyWithdrawal( + bob, + baseProceeds, + !config.enableShareWithdraws, + totalSupplyAssetsBefore, + totalSupplySharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } - // // Advance the time and accrue a large amount of interest. - // if (config.shouldAccrueInterest) { - // _variableRate = _variableRate.normalizeToRange(0, 1000e18); - // } else { - // _variableRate = 0; - // } - // advanceTime(POSITION_DURATION, int256(_variableRate)); + /// @dev Fuzz test that ensures that traders receive the correct payouts at + /// maturity when bonds are minted and burned with base. + /// @param _basePaid The fuzz parameter for the base paid. + /// @param _variableRate The fuzz parameter for the variable rate. + function test_round_trip_pair_maturity_with_base( + uint256 _basePaid, + uint256 _variableRate + ) external { + // If base deposits aren't enabled, we skip the test. + if (!config.enableBaseDeposits) { + return; + } - // // Get some balance information before the withdrawal. - // ( - // uint256 totalSupplyAssetsBefore, - // uint256 totalSupplySharesBefore - // ) = getSupply(); - // AccountBalances memory bobBalancesBefore = getAccountBalances(bob); - // AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( - // address(hyperdrive) - // ); + // Calculate the maximum amount of basePaid we can test. The limit is + // the amount of the base token that the trader has. + uint256 maxBaseAmount; + if (isBaseETH) { + maxBaseAmount = bob.balance; + } else { + maxBaseAmount = IERC20(config.baseToken).balanceOf(bob) / 10; + } - // // If base withdrawals are supported, we withdraw with base. - // uint256 baseProceeds; - // if (config.enableBaseWithdraws) { - // // Bob closes his long with base as the target asset. - // baseProceeds = closeLong(bob, maturityTime, longAmount); + // Bob mints some bonds with base. + _basePaid = _basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + maxBaseAmount + ); + (uint256 maturityTime, uint256 bondAmount) = mint(bob, _basePaid); - // // Bob should receive almost exactly his bond amount. - // assertLe( - // baseProceeds, - // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat) + - // config.roundTripLongMaturityWithBaseUpperBoundTolerance - // ); - // assertApproxEqAbs( - // baseProceeds, - // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat), - // config.roundTripLongMaturityWithBaseTolerance - // ); - // } - // // Otherwise we withdraw with vault shares. - // else { - // // Bob closes his long with vault shares as the target asset. - // uint256 vaultSharesProceeds = closeLong( - // bob, - // maturityTime, - // longAmount, - // false - // ); - // baseProceeds = hyperdrive.convertToBase(vaultSharesProceeds); + // Advance the time and accrue a large amount of interest. + if (config.shouldAccrueInterest) { + _variableRate = _variableRate.normalizeToRange(0, 2.5e18); + } else { + _variableRate = 0; + } + advanceTime(POSITION_DURATION, int256(_variableRate)); - // // Bob should receive almost exactly his bond amount. - // assertLe( - // baseProceeds, - // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat) + - // config.roundTripLongMaturityWithSharesUpperBoundTolerance - // ); - // assertApproxEqAbs( - // baseProceeds, - // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat), - // config.roundTripLongMaturityWithSharesTolerance - // ); - // } + // Get some balance information before the withdrawal. + ( + uint256 totalSupplyAssetsBefore, + uint256 totalSupplySharesBefore + ) = getSupply(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); - // // Ensure that the withdrawal was processed as expected. - // verifyWithdrawal( - // bob, - // baseProceeds, - // config.enableBaseWithdraws, - // totalSupplyAssetsBefore, - // totalSupplySharesBefore, - // bobBalancesBefore, - // hyperdriveBalancesBefore - // ); - // } + // If base withdrawals are supported, we withdraw with base. + uint256 baseProceeds; + if (config.enableBaseWithdraws) { + // Bob burns his bonds with base as the target asset. + baseProceeds = burn(bob, maturityTime, bondAmount); + + // Bob should receive almost exactly the value underlying the bonds + // minus fees. + uint256 expectedProceeds = bondAmount.mulDivDown( + hyperdrive.getCheckpoint(maturityTime).vaultSharePrice, + hyperdrive + .getCheckpoint( + maturityTime - + hyperdrive.getPoolConfig().positionDuration + ) + .vaultSharePrice + ) - + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + ONE - hyperdrive.getPoolConfig().fees.governanceLP + ) - + 2 * + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + hyperdrive.getPoolConfig().fees.governanceLP + ); + assertLe( + baseProceeds, + expectedProceeds + + config.roundTripPairMaturityWithBaseUpperBoundTolerance + ); + assertApproxEqAbs( + baseProceeds, + expectedProceeds, + config.roundTripPairMaturityWithBaseTolerance + ); + } + // Otherwise we withdraw with vault shares. + else { + // Bob burns his bonds with vault shares as the target asset. + uint256 vaultSharesProceeds = burn( + bob, + maturityTime, + bondAmount, + false + ); + baseProceeds = hyperdrive.convertToBase(vaultSharesProceeds); + + // Bob should receive almost exactly the value underlying the bonds + // minus fees. + uint256 expectedProceeds = bondAmount.mulDivDown( + hyperdrive.getCheckpoint(maturityTime).vaultSharePrice, + hyperdrive + .getCheckpoint( + maturityTime - + hyperdrive.getPoolConfig().positionDuration + ) + .vaultSharePrice + ) - + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + ONE - hyperdrive.getPoolConfig().fees.governanceLP + ) - + 2 * + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + hyperdrive.getPoolConfig().fees.governanceLP + ); + assertLe( + baseProceeds, + expectedProceeds + + config.roundTripPairMaturityWithSharesUpperBoundTolerance + ); + assertApproxEqAbs( + baseProceeds, + expectedProceeds, + config.roundTripPairMaturityWithSharesTolerance + ); + } + + // Ensure that the withdrawal was processed as expected. + verifyWithdrawal( + bob, + baseProceeds, + config.enableBaseWithdraws, + totalSupplyAssetsBefore, + totalSupplySharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } // /// @dev Fuzz test that ensures that shorts receive the correct payouts at // /// maturity when deposits and withdrawals are made with vault shares. From e6fc7f1e7f9a30eec97e85c1273cfb958eb7f878 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 16 Jan 2025 14:25:40 -1000 Subject: [PATCH 34/45] Uncommented the remaining test --- test/utils/InstanceTest.sol | 252 +++++++++++++++++++++--------------- 1 file changed, 151 insertions(+), 101 deletions(-) diff --git a/test/utils/InstanceTest.sol b/test/utils/InstanceTest.sol index e0b12ae3c..2129617c8 100644 --- a/test/utils/InstanceTest.sol +++ b/test/utils/InstanceTest.sol @@ -3992,107 +3992,157 @@ abstract contract InstanceTest is HyperdriveTest { ); } - // /// @dev Fuzz test that ensures that shorts receive the correct payouts at - // /// maturity when deposits and withdrawals are made with vault shares. - // /// @param _vaultSharesPaid The fuzz parameter for the vault shares paid. - // /// @param _variableRate The fuzz parameter for the variable rate. - // function test_round_trip_long_maturity_with_shares( - // uint256 _vaultSharesPaid, - // uint256 _variableRate - // ) external { - // // If share deposits aren't enabled, we skip the test. - // if (!config.enableShareDeposits) { - // return; - // } - - // // Bob opens a long with vault shares. - // _vaultSharesPaid = hyperdrive.convertToShares( - // _vaultSharesPaid.normalizeToRange( - // 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, - // hyperdrive.calculateMaxLong() - // ) - // ); - // (uint256 maturityTime, uint256 longAmount) = openLong( - // bob, - // _vaultSharesPaid, - // false - // ); - - // // Advance the time and accrue a large amount of interest. - // if (config.shouldAccrueInterest) { - // _variableRate = _variableRate.normalizeToRange(0, 1000e18); - // } else { - // _variableRate = 0; - // } - // advanceTime( - // hyperdrive.getPoolConfig().positionDuration, - // int256(_variableRate) - // ); - - // // Get some balance information before the withdrawal. - // ( - // uint256 totalSupplyAssetsBefore, - // uint256 totalSupplySharesBefore - // ) = getSupply(); - // AccountBalances memory bobBalancesBefore = getAccountBalances(bob); - // AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( - // address(hyperdrive) - // ); - - // // If vault share withdrawals are supported, we withdraw with vault - // // shares. - // uint256 baseProceeds; - // if (config.enableShareWithdraws) { - // // Bob closes his long with vault shares as the target asset. - // uint256 vaultSharesProceeds = closeLong( - // bob, - // maturityTime, - // longAmount, - // false - // ); - // baseProceeds = hyperdrive.convertToBase(vaultSharesProceeds); - - // // Bob should receive almost exactly his bond amount. - // assertLe( - // baseProceeds, - // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat) + - // config.roundTripLongMaturityWithSharesUpperBoundTolerance - // ); - // assertApproxEqAbs( - // baseProceeds, - // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat), - // config.roundTripLongMaturityWithSharesTolerance - // ); - // } - // // Otherwise we withdraw with base. - // else { - // // Bob closes his long with base as the target asset. - // baseProceeds = closeLong(bob, maturityTime, longAmount); - - // // Bob should receive almost exactly his bond amount. - // assertLe( - // baseProceeds, - // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat) + - // config.roundTripLongMaturityWithBaseUpperBoundTolerance - // ); - // assertApproxEqAbs( - // baseProceeds, - // longAmount.mulDown(ONE - hyperdrive.getPoolConfig().fees.flat), - // config.roundTripLongMaturityWithBaseTolerance - // ); - // } - - // // Ensure that the withdrawal was processed as expected. - // verifyWithdrawal( - // bob, - // baseProceeds, - // !config.enableShareWithdraws, - // totalSupplyAssetsBefore, - // totalSupplySharesBefore, - // bobBalancesBefore, - // hyperdriveBalancesBefore - // ); - // } + /// @dev Fuzz test that ensures that traders receive the correct payouts at + /// maturity when minting and burning with vault shares. + /// @param _vaultSharesPaid The fuzz parameter for the vault shares paid. + /// @param _variableRate The fuzz parameter for the variable rate. + function test_round_trip_pair_maturity_with_shares( + uint256 _vaultSharesPaid, + uint256 _variableRate + ) external { + // If share deposits aren't enabled, we skip the test. + if (!config.enableShareDeposits) { + return; + } + + // Calculate the maximum amount of sharesPaid we can test. The limit + // is the amount of the vault shares token that the trader has. + uint256 maxSharesAmount = IERC20(config.vaultSharesToken).balanceOf( + bob + ) / 10; + + // We normalize the basePaid variable within a valid range the market + // can support. + if (config.isRebasing) { + _vaultSharesPaid = _vaultSharesPaid.normalizeToRange( + convertToShares( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount + ), + convertToShares(maxSharesAmount) + ); + } else { + _vaultSharesPaid = _vaultSharesPaid.normalizeToRange( + convertToShares( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount + ), + maxSharesAmount + ); + } + + // Bob mints some bonds with vault shares. + (uint256 maturityTime, uint256 bondAmount) = mint( + bob, + _vaultSharesPaid, + false + ); + + // Advance the time and accrue a large amount of interest. + if (config.shouldAccrueInterest) { + _variableRate = _variableRate.normalizeToRange(0, 2.5e18); + } else { + _variableRate = 0; + } + advanceTime( + hyperdrive.getPoolConfig().positionDuration, + int256(_variableRate) + ); + + // Get some balance information before the withdrawal. + ( + uint256 totalSupplyAssetsBefore, + uint256 totalSupplySharesBefore + ) = getSupply(); + AccountBalances memory bobBalancesBefore = getAccountBalances(bob); + AccountBalances memory hyperdriveBalancesBefore = getAccountBalances( + address(hyperdrive) + ); + + // If vault share withdrawals are supported, we withdraw with vault + // shares. + uint256 baseProceeds; + if (config.enableShareWithdraws) { + // Bob burns his bonds with vault shares as the target asset. + uint256 vaultSharesProceeds = burn( + bob, + maturityTime, + bondAmount, + false + ); + baseProceeds = hyperdrive.convertToBase(vaultSharesProceeds); + + // Bob should receive almost exactly his bond amount. + uint256 expectedProceeds = bondAmount.mulDivDown( + hyperdrive.getCheckpoint(maturityTime).vaultSharePrice, + hyperdrive + .getCheckpoint( + maturityTime - + hyperdrive.getPoolConfig().positionDuration + ) + .vaultSharePrice + ) - + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + ONE - hyperdrive.getPoolConfig().fees.governanceLP + ) - + 2 * + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + hyperdrive.getPoolConfig().fees.governanceLP + ); + assertLe( + baseProceeds, + expectedProceeds + + config.roundTripPairMaturityWithSharesUpperBoundTolerance + ); + assertApproxEqAbs( + baseProceeds, + expectedProceeds, + config.roundTripPairMaturityWithSharesTolerance + ); + } + // Otherwise we withdraw with base. + else { + // Bob burns his bonds with base as the target asset. + baseProceeds = burn(bob, maturityTime, bondAmount); + + // Bob should receive almost exactly his bond amount. + uint256 expectedProceeds = bondAmount.mulDivDown( + hyperdrive.getCheckpoint(maturityTime).vaultSharePrice, + hyperdrive + .getCheckpoint( + maturityTime - + hyperdrive.getPoolConfig().positionDuration + ) + .vaultSharePrice + ) - + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + ONE - hyperdrive.getPoolConfig().fees.governanceLP + ) - + 2 * + bondAmount.mulUp(hyperdrive.getPoolConfig().fees.flat).mulDown( + hyperdrive.getPoolConfig().fees.governanceLP + ); + assertLe( + baseProceeds, + expectedProceeds + + config.roundTripPairMaturityWithBaseUpperBoundTolerance + ); + assertApproxEqAbs( + baseProceeds, + expectedProceeds, + config.roundTripPairMaturityWithBaseTolerance + ); + } + + // Ensure that the withdrawal was processed as expected. + verifyWithdrawal( + bob, + baseProceeds, + !config.enableShareWithdraws, + totalSupplyAssetsBefore, + totalSupplySharesBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } /// Sweep /// From e70d1fb95c16507e5c3ffcd3d75969c5d3c4b10e Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 17 Jan 2025 14:32:23 -1000 Subject: [PATCH 35/45] Fixed the code size issue --- contracts/src/external/HyperdriveTarget4.sol | 4 ---- foundry.toml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/contracts/src/external/HyperdriveTarget4.sol b/contracts/src/external/HyperdriveTarget4.sol index 0f0d76a58..9eafeb9c5 100644 --- a/contracts/src/external/HyperdriveTarget4.sol +++ b/contracts/src/external/HyperdriveTarget4.sol @@ -90,8 +90,6 @@ abstract contract HyperdriveTarget4 is /// Pairs /// - // FIXME: Where does this fit? - // /// @notice Mints a pair of long and short positions that directly match /// each other. The amount of long and short positions that are /// created is equal to the base value of the deposit. These @@ -116,8 +114,6 @@ abstract contract HyperdriveTarget4 is return _mint(_amount, _minOutput, _minVaultSharePrice, _options); } - // FIXME: Where does this fit? - // /// @dev Burns a pair of long and short positions that directly match each /// other. The capital underlying these positions is released to the /// trader burning the positions. diff --git a/foundry.toml b/foundry.toml index 4ef2b18da..2c2b15d8d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -37,7 +37,7 @@ evm_version = "cancun" deny_warnings = true # optimizer settings optimizer = true -optimizer_runs = 13000 +optimizer_runs = 9000 via_ir = false # Enable gas-reporting for all contracts gas_reports = ["*"] From 4e179a50e2daf3b0967537d28cc55e5f2992ef25 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 17 Jan 2025 18:19:54 -1000 Subject: [PATCH 36/45] Removed fixmes -- the investigation showed that the calculations worked correctly --- test/instances/erc4626/SUSDe.t.sol | 2 -- test/instances/erc4626/sUSDS.t.sol | 2 -- test/instances/erc4626/sxDai.t.sol | 1 - test/instances/morpho-blue/MorphoBlue_USDe_DAI_Hyperdrive.t.sol | 1 - .../instances/morpho-blue/MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol | 1 - .../morpho-blue/MorphoBlue_wstETH_USDA_Hyperdrive.t.sol | 1 - test/instances/steth/StETHHyperdrive.t.sol | 1 - 7 files changed, 9 deletions(-) diff --git a/test/instances/erc4626/SUSDe.t.sol b/test/instances/erc4626/SUSDe.t.sol index 1a57cc906..0ec364c35 100644 --- a/test/instances/erc4626/SUSDe.t.sol +++ b/test/instances/erc4626/SUSDe.t.sol @@ -113,8 +113,6 @@ contract SUSDeHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, - // FIXME: Why is this so high? Shouldn't this be lower than the - // other tolerances? roundTripPairInstantaneousWithSharesTolerance: 1e8, roundTripPairMaturityWithSharesUpperBoundTolerance: 100, roundTripPairMaturityWithSharesTolerance: 1e5, diff --git a/test/instances/erc4626/sUSDS.t.sol b/test/instances/erc4626/sUSDS.t.sol index 2fec8bb82..6f716f2df 100644 --- a/test/instances/erc4626/sUSDS.t.sol +++ b/test/instances/erc4626/sUSDS.t.sol @@ -88,7 +88,6 @@ contract sUSDSHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, - // FIXME: Why is this higher than the other tolerances? roundTripPairInstantaneousWithBaseTolerance: 1e8, roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e5, @@ -106,7 +105,6 @@ contract sUSDSHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, - // FIXME: Why is this higher than the other tolerances? roundTripPairInstantaneousWithSharesTolerance: 1e6, roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, diff --git a/test/instances/erc4626/sxDai.t.sol b/test/instances/erc4626/sxDai.t.sol index caf6a8ae3..6bc9146f3 100644 --- a/test/instances/erc4626/sxDai.t.sol +++ b/test/instances/erc4626/sxDai.t.sol @@ -93,7 +93,6 @@ contract sxDaiHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, - // FIXME: Why is this higher? roundTripPairInstantaneousWithSharesTolerance: 1e6, roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, roundTripPairMaturityWithSharesTolerance: 1e5, diff --git a/test/instances/morpho-blue/MorphoBlue_USDe_DAI_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_USDe_DAI_Hyperdrive.t.sol index f6e5c54f7..3984f3c8f 100644 --- a/test/instances/morpho-blue/MorphoBlue_USDe_DAI_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_USDe_DAI_Hyperdrive.t.sol @@ -75,7 +75,6 @@ contract MorphoBlue_USDe_DAI_HyperdriveTest is roundTripShortInstantaneousWithBaseTolerance: 1e7, roundTripShortMaturityWithBaseTolerance: 1e10, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, - // FIXME: Why is this higher than the other tolerances? roundTripPairInstantaneousWithBaseTolerance: 1e13, roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e10, diff --git a/test/instances/morpho-blue/MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol index 93ec0c86c..092556f16 100644 --- a/test/instances/morpho-blue/MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol @@ -75,7 +75,6 @@ contract MorphoBlue_sUSDe_DAI_HyperdriveTest is roundTripShortInstantaneousWithBaseTolerance: 1e8, roundTripShortMaturityWithBaseTolerance: 1e10, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, - // FIXME: Why is this higher than the other tolernances? roundTripPairInstantaneousWithBaseTolerance: 1e13, roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e10, diff --git a/test/instances/morpho-blue/MorphoBlue_wstETH_USDA_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_wstETH_USDA_Hyperdrive.t.sol index 287096c61..de44c4445 100644 --- a/test/instances/morpho-blue/MorphoBlue_wstETH_USDA_Hyperdrive.t.sol +++ b/test/instances/morpho-blue/MorphoBlue_wstETH_USDA_Hyperdrive.t.sol @@ -75,7 +75,6 @@ contract MorphoBlue_wstETH_USDA_HyperdriveTest is roundTripShortInstantaneousWithBaseTolerance: 1e8, roundTripShortMaturityWithBaseTolerance: 1e10, roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, - // FIXME: Why is this higher than the other tolerances? roundTripPairInstantaneousWithBaseTolerance: 1e13, roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, roundTripPairMaturityWithBaseTolerance: 1e10, diff --git a/test/instances/steth/StETHHyperdrive.t.sol b/test/instances/steth/StETHHyperdrive.t.sol index 56781fb47..aff424b94 100644 --- a/test/instances/steth/StETHHyperdrive.t.sol +++ b/test/instances/steth/StETHHyperdrive.t.sol @@ -101,7 +101,6 @@ contract StETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, - // FIXME: Why is this higher than the other tolerances? roundTripPairInstantaneousWithSharesTolerance: 1e5, roundTripPairMaturityWithSharesUpperBoundTolerance: 100, roundTripPairMaturityWithSharesTolerance: 1e3, From 75c561ae5880e1568096e4482e7b928cf983e70b Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 17 Jan 2025 18:23:09 -1000 Subject: [PATCH 37/45] Fixed some of the CI jobs --- contracts/src/internal/HyperdrivePair.sol | 1 - test/integrations/hyperdrive/LPWithdrawalTest.t.sol | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index bbe98ccde..9a1d0dcff 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -5,7 +5,6 @@ import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; import { IHyperdriveEvents } from "../interfaces/IHyperdriveEvents.sol"; import { AssetId } from "../libraries/AssetId.sol"; import { FixedPointMath, ONE } from "../libraries/FixedPointMath.sol"; -import { HyperdriveMath } from "../libraries/HyperdriveMath.sol"; import { LPMath } from "../libraries/LPMath.sol"; import { SafeCast } from "../libraries/SafeCast.sol"; import { HyperdriveLP } from "./HyperdriveLP.sol"; diff --git a/test/integrations/hyperdrive/LPWithdrawalTest.t.sol b/test/integrations/hyperdrive/LPWithdrawalTest.t.sol index 21808872e..d9064da85 100644 --- a/test/integrations/hyperdrive/LPWithdrawalTest.t.sol +++ b/test/integrations/hyperdrive/LPWithdrawalTest.t.sol @@ -437,7 +437,7 @@ contract LPWithdrawalTest is HyperdriveTest { uint256 lpSharePrice = hyperdrive.lpSharePrice(); uint256 baseProceeds = burn(bob, maturityTime, bondAmount); assertApproxEqAbs(baseProceeds, basePaid, 10); - assertEq(lpSharePrice, hyperdrive.lpSharePrice()); + assertApproxEqAbs(lpSharePrice, hyperdrive.lpSharePrice(), 10); // Ensure that the ending base balance of Hyperdrive only consists of // the minimum share reserves and address zero's LP shares. From 464eb410d16caf9e676ed7f7f4ce2f0eb562a14c Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 21 Jan 2025 09:08:25 -1000 Subject: [PATCH 38/45] Fixed the LPWithdrawal tests --- test/integrations/hyperdrive/LPWithdrawalTest.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integrations/hyperdrive/LPWithdrawalTest.t.sol b/test/integrations/hyperdrive/LPWithdrawalTest.t.sol index d9064da85..08a4fd2be 100644 --- a/test/integrations/hyperdrive/LPWithdrawalTest.t.sol +++ b/test/integrations/hyperdrive/LPWithdrawalTest.t.sol @@ -414,7 +414,7 @@ contract LPWithdrawalTest is HyperdriveTest { 10 * contribution ); (uint256 maturityTime, uint256 bondAmount) = mint(bob, basePaid); - assertEq(bondAmount, basePaid); + assertApproxEqAbs(bondAmount, basePaid, 10); // Alice removes all of her LP shares. The LP share price should be // approximately equal before and after the transaction. She should From 9941acb49c35f56941fbd800ea4af76102031777 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 21 Jan 2025 17:14:38 -1000 Subject: [PATCH 39/45] Attempted to fix the code coverage job --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 4e610ae8f..ac47c2753 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -53,7 +53,7 @@ jobs: run: | FOUNDRY_PROFILE=lite FOUNDRY_FUZZ_RUNS=100 forge coverage --report lcov sudo apt-get install lcov - lcov --remove lcov.info -o lcov.info 'test/*' 'script/*' + lcov --remove lcov.info -o lcov.info 'test/*' - name: Edit lcov.info run: | From eda841680d5e41dd1947e9da325d0df729aa33c8 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 21 Jan 2025 17:15:37 -1000 Subject: [PATCH 40/45] Removed the code coverage badge --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f5ec44c89..ac837c2ab 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ [![Tests](https://github.com/delvtech/hyperdrive/actions/workflows/solidity_test.yml/badge.svg)](https://github.com/delvtech/hyperdrive/actions/workflows/solidity_test.yml) -[![Coverage](https://coveralls.io/repos/github/delvtech/hyperdrive/badge.svg?branch=main&t=vnW3xG&kill_cache=1&service=github)](https://coveralls.io/github/delvtech/hyperdrive?branch=main) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/delvtech/elf-contracts/blob/master/LICENSE) [![Static Badge](https://img.shields.io/badge/DELV-Terms%20Of%20Service-orange)](https://delv-public.s3.us-east-2.amazonaws.com/delv-terms-of-service.pdf) From c2a1805af040a4a5d16d8ad2c3e01be72a9c305a Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 10 Jan 2025 17:32:37 -0600 Subject: [PATCH 41/45] Fuzzing notes --- python-fuzz/fuzz_mint_burn.py | 44 +++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/python-fuzz/fuzz_mint_burn.py b/python-fuzz/fuzz_mint_burn.py index ea14f430e..114902814 100644 --- a/python-fuzz/fuzz_mint_burn.py +++ b/python-fuzz/fuzz_mint_burn.py @@ -179,6 +179,12 @@ def main(argv: Sequence[str] | None = None) -> None: log_to_rollbar=log_to_rollbar, ignore_raise_error_func=_fuzz_ignore_errors, random_advance_time=False, # We take care of advancing time in the outer loop + # FIXME: Parameterize this. Would be good to go up to 250%. + # + # FIXME: Might be better to just call this myself. + # + # https://github.com/delvtech/agent0/blob/f3bc4c71b98d3cf407a18f62de85dab3fc63eb62/src/agent0/hyperfuzz/system_fuzz/run_fuzz_bots.py#L415 + random_variable_rate=True, # Variable rate can change between 0% and 100% lp_share_price_test=False, base_budget_per_bot=FixedPoint(1_000_000), num_iterations=1, @@ -194,13 +200,17 @@ def main(argv: Sequence[str] | None = None) -> None: trade = chain.config.rng.choice(["mint", "burn"]) # type: ignore match trade: case "mint": + # FIXME: Need to add the pool that I need to call. balance = agent.get_wallet().balance.amount - if balance > 0: + # FIXME: Double check this. + if balance > hyperdrive_config.minimum_transaction_amount: # TODO can't use numpy rng since it doesn't support uint256. # Need to use the state from the chain config to use the same rng object. amount = random.randint(0, balance.scaled_value) logging.info(f"Agent {agent.address} is calling minting with {amount}") + # FIXME: This should work + # # FIXME figure out what these options are pair_options = PairOptions( longDestination=agent.address, @@ -209,31 +219,35 @@ def main(argv: Sequence[str] | None = None) -> None: extraData=bytes(0), ) + # FIXME: I need to make sure that agent0 knows that + # the minted positions are owned. hyperdrive_contract.functions.mint( _amount=amount, _minOutput=0, _minVaultSharePrice=0, _options=pair_options ).sign_transact_and_wait(account=agent.account, validate_transaction=True) case "burn": + # FIXME: Update this to ensure that they have an equal + # amount of longs and shorts in a given maturity. Can + # also burn a partial amount + # # FIXME figure out in what cases an agent can burn tokens + # + # FIXME: This may return a list, but I can look at other + # functions to get a dictionary keyed by maturity time agent_longs = agent.get_longs() num_longs = len(agent_longs) if num_longs > 0 and agent_longs[0].balance > 0: amount = random.randint(0, balance.scaled_value) logging.info(f"Agent {agent.address} is calling burn with {amount}") - - # FIXME figure out what these options are - # pair_options = PairOptions( - # longDestination=agent.address, - # shortDestination=agent.address, - # asBase=True, - # extraData=bytes(0), - # ) options = Options( destination=agent.address, asBase=True, extraData=bytes(0), ) + # FIXME: Update the parameters. Fuzz over the amount + # of bonds to burn. + # # FIXME figure out what _maturityTime is # FIXME burn is expecting `Options`, not `PairOptions` hyperdrive_contract.functions.burn( @@ -241,7 +255,17 @@ def main(argv: Sequence[str] | None = None) -> None: ).sign_transact_and_wait(account=agent.account, validate_transaction=True) # FIXME add any additional invariance checks specific to mint/burn here. - + # + # FIXME: I don't think there are any other invariant checks. I just + # need to make sure that I can run the existing invariant checks. + # + # FIXME: Invariant checks are abstracted into a function. I should + # call that function here. Here's the link: + # + # https://github.com/delvtech/agent0/blob/f3bc4c71b98d3cf407a18f62de85dab3fc63eb62/src/agent0/hyperfuzz/system_fuzz/run_fuzz_bots.py#L460 + + # FIXME: Tweak this time. + # # Advance time for a day # TODO parameterize the amount of time to advance. chain.advance_time(60 * 60 * 24) From 78cb0cf6bafcba18ebee81863982cb1ab1946a8b Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 22 Jan 2025 13:57:34 -1000 Subject: [PATCH 42/45] Got a working prototype of the `mint` and `burn` fuzzing working --- .../src/interfaces/IHyperdriveEvents.sol | 4 +- contracts/src/internal/HyperdrivePair.sol | 4 +- python-fuzz/fuzz_mint_burn.py | 60 ++++++++++--------- python-fuzz/requirements.txt | 2 +- test/units/hyperdrive/BurnTest.t.sol | 2 +- test/units/hyperdrive/MintTest.t.sol | 2 +- 6 files changed, 38 insertions(+), 36 deletions(-) diff --git a/contracts/src/interfaces/IHyperdriveEvents.sol b/contracts/src/interfaces/IHyperdriveEvents.sol index cb7c60cc2..2770e4d18 100644 --- a/contracts/src/interfaces/IHyperdriveEvents.sol +++ b/contracts/src/interfaces/IHyperdriveEvents.sol @@ -103,7 +103,7 @@ interface IHyperdriveEvents is IMultiTokenEvents { ); /// @notice Emitted when a pair of long and short positions are minted. - event Mint( + event MintBonds( address indexed longTrader, address indexed shortTrader, uint256 indexed maturityTime, @@ -117,7 +117,7 @@ interface IHyperdriveEvents is IMultiTokenEvents { ); /// @notice Emitted when a pair of long and short positions are burned. - event Burn( + event BurnBonds( address indexed trader, address indexed destination, uint256 indexed maturityTime, diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol index 9a1d0dcff..63203be0b 100644 --- a/contracts/src/internal/HyperdrivePair.sol +++ b/contracts/src/internal/HyperdrivePair.sol @@ -124,7 +124,7 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { uint256 bondAmount_ = bondAmount; // avoid stack-too-deep uint256 amount = _amount; // avoid stack-too-deep IHyperdrive.PairOptions calldata options = _options; // avoid stack-too-deep - emit Mint( + emit MintBonds( options.longDestination, options.shortDestination, maturityTime, @@ -252,7 +252,7 @@ abstract contract HyperdrivePair is IHyperdriveEvents, HyperdriveLP { // Emit a Burn event. uint256 bondAmount = _bondAmount; // avoid stack-too-deep IHyperdrive.Options calldata options = _options; // avoid stack-too-deep - emit Burn( + emit BurnBonds( msg.sender, options.destination, _maturityTime, diff --git a/python-fuzz/fuzz_mint_burn.py b/python-fuzz/fuzz_mint_burn.py index 114902814..da9d7353f 100644 --- a/python-fuzz/fuzz_mint_burn.py +++ b/python-fuzz/fuzz_mint_burn.py @@ -201,7 +201,7 @@ def main(argv: Sequence[str] | None = None) -> None: match trade: case "mint": # FIXME: Need to add the pool that I need to call. - balance = agent.get_wallet().balance.amount + balance = agent.get_wallet(hyperdrive_pool).balance.amount # FIXME: Double check this. if balance > hyperdrive_config.minimum_transaction_amount: # TODO can't use numpy rng since it doesn't support uint256. @@ -225,34 +225,36 @@ def main(argv: Sequence[str] | None = None) -> None: _amount=amount, _minOutput=0, _minVaultSharePrice=0, _options=pair_options ).sign_transact_and_wait(account=agent.account, validate_transaction=True) - case "burn": - # FIXME: Update this to ensure that they have an equal - # amount of longs and shorts in a given maturity. Can - # also burn a partial amount - # - # FIXME figure out in what cases an agent can burn tokens - # - # FIXME: This may return a list, but I can look at other - # functions to get a dictionary keyed by maturity time - agent_longs = agent.get_longs() - num_longs = len(agent_longs) - if num_longs > 0 and agent_longs[0].balance > 0: - amount = random.randint(0, balance.scaled_value) - logging.info(f"Agent {agent.address} is calling burn with {amount}") - options = Options( - destination=agent.address, - asBase=True, - extraData=bytes(0), - ) - - # FIXME: Update the parameters. Fuzz over the amount - # of bonds to burn. - # - # FIXME figure out what _maturityTime is - # FIXME burn is expecting `Options`, not `PairOptions` - hyperdrive_contract.functions.burn( - _maturityTime=0, _bondAmount=0, _minOutput=0, _options=options - ).sign_transact_and_wait(account=agent.account, validate_transaction=True) + # FIXME: Add this case back. + # + # case "burn": + # # FIXME: Update this to ensure that they have an equal + # # amount of longs and shorts in a given maturity. Can + # # also burn a partial amount + # # + # # FIXME figure out in what cases an agent can burn tokens + # # + # # FIXME: This may return a list, but I can look at other + # # functions to get a dictionary keyed by maturity time + # agent_longs = agent.get_longs(hyperdrive_pool) + # num_longs = len(agent_longs) + # if num_longs > 0 and agent_longs[0].balance > 0: + # amount = random.randint(0, balance.scaled_value) + # logging.info(f"Agent {agent.address} is calling burn with {amount}") + # options = Options( + # destination=agent.address, + # asBase=True, + # extraData=bytes(0), + # ) + + # # FIXME: Update the parameters. Fuzz over the amount + # # of bonds to burn. + # # + # # FIXME figure out what _maturityTime is + # # FIXME burn is expecting `Options`, not `PairOptions` + # hyperdrive_contract.functions.burn( + # _maturityTime=0, _bondAmount=0, _minOutput=0, _options=options + # ).sign_transact_and_wait(account=agent.account, validate_transaction=True) # FIXME add any additional invariance checks specific to mint/burn here. # diff --git a/python-fuzz/requirements.txt b/python-fuzz/requirements.txt index e803a1d22..bd23c89f2 100644 --- a/python-fuzz/requirements.txt +++ b/python-fuzz/requirements.txt @@ -1,2 +1,2 @@ -e python/hyperdrivetypes -agent0 >= 0.26.2 \ No newline at end of file +-e ../agent0 diff --git a/test/units/hyperdrive/BurnTest.t.sol b/test/units/hyperdrive/BurnTest.t.sol index 14a0e531a..688e80576 100644 --- a/test/units/hyperdrive/BurnTest.t.sol +++ b/test/units/hyperdrive/BurnTest.t.sol @@ -631,7 +631,7 @@ contract BurnTest is HyperdriveTest { uint256 _proceeds ) internal { VmSafe.Log[] memory logs = vm.getRecordedLogs().filterLogs( - Burn.selector + BurnBonds.selector ); assertEq(logs.length, 1); VmSafe.Log memory log = logs[0]; diff --git a/test/units/hyperdrive/MintTest.t.sol b/test/units/hyperdrive/MintTest.t.sol index 0835f157b..b557d0eb5 100644 --- a/test/units/hyperdrive/MintTest.t.sol +++ b/test/units/hyperdrive/MintTest.t.sol @@ -473,7 +473,7 @@ contract MintTest is HyperdriveTest { uint256 _bondAmount ) internal { VmSafe.Log[] memory logs = vm.getRecordedLogs().filterLogs( - Mint.selector + MintBonds.selector ); assertEq(logs.length, 1); VmSafe.Log memory log = logs[0]; From b85c98d822e820d4eaead43be67c498351a77f27 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 23 Jan 2025 13:09:20 -1000 Subject: [PATCH 43/45] Added the `burn` route to the fuzz bots --- python-fuzz/README.md | 2 +- python-fuzz/fuzz_mint_burn.py | 79 +++++++++++++++++------------------ 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/python-fuzz/README.md b/python-fuzz/README.md index d8fa73bd9..5fd788d23 100644 --- a/python-fuzz/README.md +++ b/python-fuzz/README.md @@ -7,7 +7,7 @@ This directory details how to install and run fuzzing on hyperdrive with mint/bu First, compile the solidity contracts and make python types locally via `make`. Next, follow the prerequisites installation instructions of [agent0](https://github.com/delvtech/agent0/blob/main/INSTALL.md). -Then install [uv](https://github.com/astral-sh/uv) for package management. No need to clone the repo locally +Then install [uv](https://github.com/astral-sh/uv) for package management. No need to clone the repo locally (unless developing on agent0). From the base directory of the `hyperdrive` repo, set up a python virtual environment: diff --git a/python-fuzz/fuzz_mint_burn.py b/python-fuzz/fuzz_mint_burn.py index da9d7353f..1318728ef 100644 --- a/python-fuzz/fuzz_mint_burn.py +++ b/python-fuzz/fuzz_mint_burn.py @@ -200,61 +200,58 @@ def main(argv: Sequence[str] | None = None) -> None: trade = chain.config.rng.choice(["mint", "burn"]) # type: ignore match trade: case "mint": - # FIXME: Need to add the pool that I need to call. balance = agent.get_wallet(hyperdrive_pool).balance.amount - # FIXME: Double check this. if balance > hyperdrive_config.minimum_transaction_amount: # TODO can't use numpy rng since it doesn't support uint256. # Need to use the state from the chain config to use the same rng object. - amount = random.randint(0, balance.scaled_value) - logging.info(f"Agent {agent.address} is calling minting with {amount}") - - # FIXME: This should work - # - # FIXME figure out what these options are + amount = random.randint(hyperdrive_config.minimum_transaction_amount.scaled_value, balance.scaled_value) pair_options = PairOptions( longDestination=agent.address, shortDestination=agent.address, asBase=True, extraData=bytes(0), ) - - # FIXME: I need to make sure that agent0 knows that - # the minted positions are owned. hyperdrive_contract.functions.mint( _amount=amount, _minOutput=0, _minVaultSharePrice=0, _options=pair_options ).sign_transact_and_wait(account=agent.account, validate_transaction=True) - # FIXME: Add this case back. - # - # case "burn": - # # FIXME: Update this to ensure that they have an equal - # # amount of longs and shorts in a given maturity. Can - # # also burn a partial amount - # # - # # FIXME figure out in what cases an agent can burn tokens - # # - # # FIXME: This may return a list, but I can look at other - # # functions to get a dictionary keyed by maturity time - # agent_longs = agent.get_longs(hyperdrive_pool) - # num_longs = len(agent_longs) - # if num_longs > 0 and agent_longs[0].balance > 0: - # amount = random.randint(0, balance.scaled_value) - # logging.info(f"Agent {agent.address} is calling burn with {amount}") - # options = Options( - # destination=agent.address, - # asBase=True, - # extraData=bytes(0), - # ) - - # # FIXME: Update the parameters. Fuzz over the amount - # # of bonds to burn. - # # - # # FIXME figure out what _maturityTime is - # # FIXME burn is expecting `Options`, not `PairOptions` - # hyperdrive_contract.functions.burn( - # _maturityTime=0, _bondAmount=0, _minOutput=0, _options=options - # ).sign_transact_and_wait(account=agent.account, validate_transaction=True) + case "burn": + wallet = agent.get_wallet(hyperdrive_pool) + + # Find maturity times that have both long and short positions + matching_maturities = set(wallet.longs.keys()) & set(wallet.shorts.keys()) + + if matching_maturities: + selected_maturity = random.choice(list(matching_maturities)) + + # Get positions for selected maturity + long_balance = wallet.longs[selected_maturity].balance + short_balance = wallet.shorts[selected_maturity].balance + max_burnable = min(long_balance, short_balance) + + if max_burnable > hyperdrive_config.minimum_transaction_amount: + burn_amount = random.randint( + hyperdrive_config.minimum_transaction_amount.scaled_value, + max_burnable.scaled_value + ) + logging.info( + f"Agent {agent.address} is burning {burn_amount} of positions " + f"with maturity time {selected_maturity}" + ) + options = Options( + destination=agent.address, + asBase=True, + extraData=bytes(0) + ) + hyperdrive_contract.functions.burn( + _maturityTime=selected_maturity, + _bondAmount=burn_amount, + _minOutput=0, + _options=options + ).sign_transact_and_wait( + account=agent.account, + validate_transaction=True + ) # FIXME add any additional invariance checks specific to mint/burn here. # From 52fd8771319f891c986bff27c6d586ead383320e Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 24 Jan 2025 13:26:48 -1000 Subject: [PATCH 44/45] Updated remaining FIXMEs --- python-fuzz/fuzz_mint_burn.py | 243 ++++++++++++++++------------------ 1 file changed, 116 insertions(+), 127 deletions(-) diff --git a/python-fuzz/fuzz_mint_burn.py b/python-fuzz/fuzz_mint_burn.py index 1318728ef..a48599938 100644 --- a/python-fuzz/fuzz_mint_burn.py +++ b/python-fuzz/fuzz_mint_burn.py @@ -132,142 +132,131 @@ def main(argv: Sequence[str] | None = None) -> None: gas_limit=int(1e6), # Plenty of gas limit for transactions ) - # FIXME wrap all of this in a try catch to catch any exceptions thrown in fuzzing. - # When an error occurs, we likely want to pause the chain to allow for remote connection - # for debugging while True: # Build interactive local hyperdrive # TODO can likely reuse some of these resources # instead, we start from scratch every time. chain = LocalChain(local_chain_config) - # Fuzz over config values - hyperdrive_config = generate_fuzz_hyperdrive_config(rng, lp_share_price_test=False, steth=False) - try: - hyperdrive_pool = LocalHyperdrive(chain, hyperdrive_config) - except Exception as e: # pylint: disable=broad-except - logging.error( - "Error deploying hyperdrive: %s", - repr(e), - ) - log_rollbar_exception( - e, - log_level=logging.ERROR, - rollbar_log_prefix="Error deploying hyperdrive poolError deploying hyperdrive pool", - ) - chain.cleanup() - continue - - agents = None - - # Run the fuzzing bot for an episode - for _ in range(parsed_args.num_iterations_per_episode): - # Run fuzzing via agent0 function on underlying hyperdrive pool. - # By default, this sets up 4 agents. - # `check_invariance` also runs the pool's invariance checks after trades. - # We only run for 1 iteration here, as we want to make additional random trades - # wrt mint/burn. - agents = run_fuzz_bots( - chain, - hyperdrive_pools=[hyperdrive_pool], - # We pass in the same agents when running fuzzing - agents=agents, - check_invariance=True, - raise_error_on_failed_invariance_checks=True, - raise_error_on_crash=True, - log_to_rollbar=log_to_rollbar, - ignore_raise_error_func=_fuzz_ignore_errors, - random_advance_time=False, # We take care of advancing time in the outer loop - # FIXME: Parameterize this. Would be good to go up to 250%. - # - # FIXME: Might be better to just call this myself. - # - # https://github.com/delvtech/agent0/blob/f3bc4c71b98d3cf407a18f62de85dab3fc63eb62/src/agent0/hyperfuzz/system_fuzz/run_fuzz_bots.py#L415 - random_variable_rate=True, # Variable rate can change between 0% and 100% - lp_share_price_test=False, - base_budget_per_bot=FixedPoint(1_000_000), - num_iterations=1, - minimum_avg_agent_base=FixedPoint(100_000), - ) - - # Get access to the underlying hyperdrive contract for pypechain calls - hyperdrive_contract = hyperdrive_pool.interface.hyperdrive_contract - - # Run random vault mint/burn - for agent in agents: - # Pick mint or burn at random - trade = chain.config.rng.choice(["mint", "burn"]) # type: ignore - match trade: - case "mint": - balance = agent.get_wallet(hyperdrive_pool).balance.amount - if balance > hyperdrive_config.minimum_transaction_amount: - # TODO can't use numpy rng since it doesn't support uint256. - # Need to use the state from the chain config to use the same rng object. - amount = random.randint(hyperdrive_config.minimum_transaction_amount.scaled_value, balance.scaled_value) - pair_options = PairOptions( - longDestination=agent.address, - shortDestination=agent.address, - asBase=True, - extraData=bytes(0), - ) - hyperdrive_contract.functions.mint( - _amount=amount, _minOutput=0, _minVaultSharePrice=0, _options=pair_options - ).sign_transact_and_wait(account=agent.account, validate_transaction=True) - - case "burn": - wallet = agent.get_wallet(hyperdrive_pool) - - # Find maturity times that have both long and short positions - matching_maturities = set(wallet.longs.keys()) & set(wallet.shorts.keys()) - - if matching_maturities: - selected_maturity = random.choice(list(matching_maturities)) - - # Get positions for selected maturity - long_balance = wallet.longs[selected_maturity].balance - short_balance = wallet.shorts[selected_maturity].balance - max_burnable = min(long_balance, short_balance) - - if max_burnable > hyperdrive_config.minimum_transaction_amount: - burn_amount = random.randint( - hyperdrive_config.minimum_transaction_amount.scaled_value, - max_burnable.scaled_value - ) - logging.info( - f"Agent {agent.address} is burning {burn_amount} of positions " - f"with maturity time {selected_maturity}" - ) - options = Options( - destination=agent.address, + # Fuzz over config values + hyperdrive_config = generate_fuzz_hyperdrive_config(rng, lp_share_price_test=False, steth=False) + + try: + hyperdrive_pool = LocalHyperdrive(chain, hyperdrive_config) + except Exception as e: # pylint: disable=broad-except + logging.error( + "Error deploying hyperdrive: %s", + repr(e), + ) + log_rollbar_exception( + e, + log_level=logging.ERROR, + rollbar_log_prefix="Error deploying hyperdrive poolError deploying hyperdrive pool", + ) + chain.cleanup() + continue + + agents = None + + # Run the fuzzing bot for an episode + for _ in range(parsed_args.num_iterations_per_episode): + # Run fuzzing via agent0 function on underlying hyperdrive pool. + # By default, this sets up 4 agents. + # `check_invariance` also runs the pool's invariance checks after trades. + # We only run for 1 iteration here, as we want to make additional random trades + # wrt mint/burn. + agents = run_fuzz_bots( + chain, + hyperdrive_pools=[hyperdrive_pool], + # We pass in the same agents when running fuzzing + agents=agents, + check_invariance=True, + raise_error_on_failed_invariance_checks=True, + raise_error_on_crash=True, + log_to_rollbar=log_to_rollbar, + ignore_raise_error_func=_fuzz_ignore_errors, + random_advance_time=True, + random_variable_rate=True, # Variable rate can change between 0% and 100% + lp_share_price_test=False, + base_budget_per_bot=FixedPoint(1_000_000), + num_iterations=1, + minimum_avg_agent_base=FixedPoint(100_000), + ) + + # Get access to the underlying hyperdrive contract for pypechain calls + hyperdrive_contract = hyperdrive_pool.interface.hyperdrive_contract + + # Run random vault mint/burn + for agent in agents: + # Pick mint or burn at random + trade = chain.config.rng.choice(["mint", "burn"]) # type: ignore + match trade: + case "mint": + balance = agent.get_wallet(hyperdrive_pool).balance.amount + if balance > hyperdrive_config.minimum_transaction_amount: + # TODO can't use numpy rng since it doesn't support uint256. + # Need to use the state from the chain config to use the same rng object. + amount = random.randint(hyperdrive_config.minimum_transaction_amount.scaled_value, balance.scaled_value) + pair_options = PairOptions( + longDestination=agent.address, + shortDestination=agent.address, asBase=True, - extraData=bytes(0) - ) - hyperdrive_contract.functions.burn( - _maturityTime=selected_maturity, - _bondAmount=burn_amount, - _minOutput=0, - _options=options - ).sign_transact_and_wait( - account=agent.account, - validate_transaction=True + extraData=bytes(0), ) - - # FIXME add any additional invariance checks specific to mint/burn here. - # - # FIXME: I don't think there are any other invariant checks. I just - # need to make sure that I can run the existing invariant checks. - # - # FIXME: Invariant checks are abstracted into a function. I should - # call that function here. Here's the link: - # - # https://github.com/delvtech/agent0/blob/f3bc4c71b98d3cf407a18f62de85dab3fc63eb62/src/agent0/hyperfuzz/system_fuzz/run_fuzz_bots.py#L460 - - # FIXME: Tweak this time. - # - # Advance time for a day - # TODO parameterize the amount of time to advance. - chain.advance_time(60 * 60 * 24) + hyperdrive_contract.functions.mint( + _amount=amount, _minOutput=0, _minVaultSharePrice=0, _options=pair_options + ).sign_transact_and_wait(account=agent.account, validate_transaction=True) + + case "burn": + wallet = agent.get_wallet(hyperdrive_pool) + + # Find maturity times that have both long and short positions + matching_maturities = set(wallet.longs.keys()) & set(wallet.shorts.keys()) + + if matching_maturities: + selected_maturity = random.choice(list(matching_maturities)) + + # Get positions for selected maturity + long_balance = wallet.longs[selected_maturity].balance + short_balance = wallet.shorts[selected_maturity].balance + max_burnable = min(long_balance, short_balance) + + if max_burnable > hyperdrive_config.minimum_transaction_amount: + burn_amount = random.randint( + hyperdrive_config.minimum_transaction_amount.scaled_value, + max_burnable.scaled_value + ) + logging.info( + f"Agent {agent.address} is burning {burn_amount} of positions " + f"with maturity time {selected_maturity}" + ) + options = Options( + destination=agent.address, + asBase=True, + extraData=bytes(0) + ) + hyperdrive_contract.functions.burn( + _maturityTime=selected_maturity, + _bondAmount=burn_amount, + _minOutput=0, + _options=options + ).sign_transact_and_wait( + account=agent.account, + validate_transaction=True + ) + + # Catch any exceptions and pause until user input is provided. + except Exception as e: + logging.error("Error during fuzzing: %s", repr(e)) + log_rollbar_exception(e, log_level=logging.ERROR, rollbar_log_prefix="Fuzzing error") + + # Keep anvil running and wait for debug connection + input("Press Enter to continue after debugging...") + + # Cleanup and start fresh iteration + chain.cleanup() + continue class Args(NamedTuple): From 3b8f249f04f151c43a4c96e5c96cba2b7cf1487a Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Fri, 24 Jan 2025 13:32:48 -1000 Subject: [PATCH 45/45] Removed newlines --- python-fuzz/README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/python-fuzz/README.md b/python-fuzz/README.md index 5fd788d23..ec6c5ad3c 100644 --- a/python-fuzz/README.md +++ b/python-fuzz/README.md @@ -30,6 +30,3 @@ To run fuzzing, simply run the `fuzz_mint_burn.py` script: ``` python fuzz_mint_burn.py ``` - - -