From 715e4385651c4c8622c5f103652af325607a7297 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 23 Jan 2025 07:24:48 -1000 Subject: [PATCH] Adds `mint` and `burn` functions (#1220) * Adds an `openPair` function * Removes some outdated comments * Added governance fees to the mint function * Updated the implementation of `HyperdrivePair` * Updated the `mint` logic and wired it up * Added unit test cases for the `mint` function * Adds comprehensive unit tests for the mint function * Adds a `minOutput` parameter to `mint` * Wrote a comprehensive integration test suite for the `mint` function * Started implementing `burn` * Made some targeted fixes to `mint` * Added zombie interest to the `burn` flow and cleaned up `HyperdrivePair` * Started adding a test suite for the `burn` function * Added the remaining test cases. Some of them are broken. * Fixed the remaining `burn` unit tests * Added an integration test suite for `burn` * Bumping solidity version of mint to match rest of repo (#1235) * Added tests for zombie interest for `mint` and `burn` * Added more integration tests for `mint` and `burn` * Added a negative interest test for `mint` * Addressed review feedback from @Sean329 * Committed incremental progress * Added more `mint` and `burn` related cases to the `InstanceTest` suite * Fixed the failing `test_burn_with_base` tests * Added another instance test and got all of the tests working * Uncommented and fixed another test * Uncommented the remaining test * Fixed the code size issue * Removed fixmes -- the investigation showed that the calculations worked correctly * Fixed some of the CI jobs * Fixed the LPWithdrawal tests * Attempted to fix the code coverage job * Removed the code coverage badge * Fixed a rare issue in the fixed point math tests --------- Co-authored-by: Sheng Lundquist --- .github/workflows/coverage.yml | 2 +- README.md | 1 - contracts/src/external/Hyperdrive.sol | 22 + 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 | 47 + contracts/src/interfaces/IHyperdrive.sol | 15 + contracts/src/interfaces/IHyperdriveCore.sol | 41 + .../src/interfaces/IHyperdriveEvents.sol | 28 + contracts/src/internal/HyperdriveBase.sol | 42 +- contracts/src/internal/HyperdriveLP.sol | 6 +- contracts/src/internal/HyperdriveLong.sol | 3 +- contracts/src/internal/HyperdrivePair.sol | 563 +++++++++ contracts/src/internal/HyperdriveShort.sol | 2 +- contracts/test/MockERC4626Hyperdrive.sol | 2 +- foundry.toml | 2 +- python/gas_benchmarks.py | 2 + test/instances/aave/AaveHyperdrive.t.sol | 23 + test/instances/aave/AaveL2Hyperdrive.t.sol | 40 +- .../AerodromeLp_AERO_USDC_Hyperdrive.t.sol | 10 + test/instances/chainlink/CbETHBase.t.sol | 16 +- .../chainlink/WstETHGnosisChain.t.sol | 16 +- test/instances/corn/Corn_LBTC_Hyperdrive.sol | 10 + .../instances/corn/Corn_sDAI_Hyperdrive.t.sol | 10 + test/instances/eeth/EETHHyperdrive.t.sol | 10 + test/instances/erc4626/MoonwellETH.t.sol | 10 + test/instances/erc4626/MoonwellEURC.t.sol | 10 + test/instances/erc4626/MoonwellUSDC.t.sol | 10 + test/instances/erc4626/SUSDe.t.sol | 10 + test/instances/erc4626/ScrvUSD.t.sol | 10 + test/instances/erc4626/SnARS.t.sol | 10 + test/instances/erc4626/StUSD.t.sol | 10 + test/instances/erc4626/sGYD.t.sol | 10 + test/instances/erc4626/sGYD_gnosis.t.sol | 10 + test/instances/erc4626/sUSDS.t.sol | 22 +- test/instances/erc4626/sxDai.t.sol | 10 + test/instances/ezETH/EzETHHyperdrive.t.sol | 14 +- .../ezeth-linea/EzETHLineaTest.t.sol | 10 + test/instances/lseth/LsETHHyperdrive.t.sol | 16 +- .../MorphoBlue_USDe_DAI_Hyperdrive.t.sol | 10 + .../MorphoBlue_WBTC_USDC_Hyperdrive.t.sol | 10 + ...orphoBlue_cbETH_USDC_Base_Hyperdrive.t.sol | 16 +- ...hoBlue_cbETH_USDC_Mainnet_Hyperdrive.t.sol | 10 + .../MorphoBlue_sUSDe_DAI_Hyperdrive.t.sol | 10 + .../MorphoBlue_wstETH_USDA_Hyperdrive.t.sol | 10 + .../MorphoBlue_wstETH_USDC_Hyperdrive.t.sol | 10 + test/instances/reth/RETHHyperdrive.t.sol | 10 + .../rseth-linea/RsETHLineaHyperdrive.t.sol | 10 + .../SavingsUSDS_Base_Hyperdrive.t.sol | 10 + .../StakingUSDS_Chronicle_Hyperdrive.t.sol | 10 + .../StakingUSDS_Sky_Hyperdrive.t.sol | 10 + test/instances/steth/StETHHyperdrive.t.sol | 16 +- .../stk-well/StkWellHyperdrive.t.sol | 10 + test/integrations/hyperdrive/BurnTest.t.sol | 294 +++++ .../IntraCheckpointNettingTest.t.sol | 77 ++ .../hyperdrive/LPWithdrawalTest.t.sol | 167 +++ test/integrations/hyperdrive/MintTest.t.sol | 271 +++++ .../hyperdrive/NonstandardDecimals.sol | 96 ++ .../hyperdrive/ReentrancyTest.t.sol | 90 +- .../hyperdrive/RoundTripTest.t.sol | 156 ++- .../hyperdrive/SandwichTest.t.sol | 98 ++ .../hyperdrive/ZombieInterestTest.t.sol | 237 ++++ test/units/hyperdrive/BurnTest.t.sol | 676 ++++++++++ test/units/hyperdrive/CloseLongTest.t.sol | 10 +- test/units/hyperdrive/CloseShortTest.t.sol | 28 +- test/units/hyperdrive/MintTest.t.sol | 515 ++++++++ test/units/hyperdrive/OpenLongTest.t.sol | 1 - test/units/libraries/FixedPointMath.t.sol | 11 +- test/utils/HyperdriveTest.sol | 147 +++ test/utils/InstanceTest.sol | 1084 +++++++++++++++++ 72 files changed, 5133 insertions(+), 60 deletions(-) create mode 100644 contracts/src/internal/HyperdrivePair.sol create mode 100644 test/integrations/hyperdrive/BurnTest.t.sol create mode 100644 test/integrations/hyperdrive/MintTest.t.sol create mode 100644 test/units/hyperdrive/BurnTest.t.sol create mode 100644 test/units/hyperdrive/MintTest.t.sol 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: | 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) diff --git a/contracts/src/external/Hyperdrive.sol b/contracts/src/external/Hyperdrive.sol index d212f5f47..945376b65 100644 --- a/contracts/src/external/Hyperdrive.sol +++ b/contracts/src/external/Hyperdrive.sol @@ -244,6 +244,28 @@ abstract contract Hyperdrive is _delegate(target4); } + /// Pairs /// + + /// @inheritdoc IHyperdriveCore + function mint( + uint256, + uint256, + uint256, + IHyperdrive.PairOptions calldata + ) external payable returns (uint256, uint256) { + _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/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..9eafeb9c5 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,51 @@ abstract contract HyperdriveTarget4 is ); } + /// 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 _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, _minOutput, _minVaultSharePrice, _options); + } + + /// @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/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/IHyperdriveCore.sol b/contracts/src/interfaces/IHyperdriveCore.sol index 5301e45eb..2209a95f6 100644 --- a/contracts/src/interfaces/IHyperdriveCore.sol +++ b/contracts/src/interfaces/IHyperdriveCore.sol @@ -162,6 +162,47 @@ 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 _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); + + /// @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 4a8b8ca13..cb7c60cc2 100644 --- a/contracts/src/interfaces/IHyperdriveEvents.sol +++ b/contracts/src/interfaces/IHyperdriveEvents.sol @@ -102,6 +102,34 @@ interface IHyperdriveEvents is IMultiTokenEvents { bytes extraData ); + /// @notice Emitted when a pair of long and short positions are minted. + event Mint( + 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 pair of long and short positions are burned. + event Burn( + address indexed trader, + address indexed destination, + 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..9a1d0dcff --- /dev/null +++ b/contracts/src/internal/HyperdrivePair.sol @@ -0,0 +1,563 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.24; + +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 { LPMath } from "../libraries/LPMath.sol"; +import { SafeCast } from "../libraries/SafeCast.sol"; +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, HyperdriveLP { + using FixedPointMath for uint256; + using FixedPointMath for int256; + using SafeCast for uint256; + using SafeCast for int256; + + /// @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. + /// @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 + ) + 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. + (uint256 sharesDeposited, uint256 vaultSharePrice) = _deposit( + _amount, + _options.asBase, + _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(); + } + + // Perform a checkpoint. + uint256 latestCheckpoint = _latestCheckpoint(); + 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 + ); + + // Enforce the minimum user outputs. + if (bondAmount < _minOutput) { + revert IHyperdrive.OutputLimit(); + } + + // Apply the state changes caused by minting the offsetting longs and + // shorts. + maturityTime = latestCheckpoint + _positionDuration; + _applyMint(maturityTime, bondAmount, governanceFee); + + // 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 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 + emit Mint( + options.longDestination, + options.shortDestination, + maturityTime, + longAssetId, + shortAssetId, + amount, + vaultSharePrice, + options.asBase, + bondAmount_, + options.extraData + ); + + return (maturityTime, 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 + ) 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(); + } + + // 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) { + _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 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. + 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. + _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 + // 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. + 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 + ); + + return proceeds; + } + + /// @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 _bondAmount The amount of bonds created. + /// @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. + _governanceFeesAccrued += _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, + 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(); + } + + /// @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 unchanged because the positions fully net out. + /// Specifically: + /// + /// - 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. + /// + /// Therefore: + /// + /// - Solvency checks are unnecessary. + /// + /// 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 _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. + _governanceFeesAccrued += _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(); + + // 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 + /// 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. + /// @return The amount of bonds to mint. + /// @return The governance fee in shares charged to the depositor. + 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 * (max(c, c0) - c0) / c0 + bondAmount * flatFee + + // 2 * bondAmount * flatFee * governanceFee + // ) + // + // This implies that: + // + // bondAmount = shareDeposited * vaultSharePrice / ( + // max(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. + // + // 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)) + ); + + // The governance fee that will be paid on the long and the short + // sides of the trade in shares is given by: + // + // 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 = 2 * + bondAmount.mulUp(_flatFee).mulDivDown( + _governanceLPFee, + _vaultSharePrice + ); + + return (bondAmount, governanceFee); + } + + /// @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, uint256) { + // The short's pre-paid flat fee in shares that will be refunded. This + // is given by: + // + // prepaidFlatFee = bondAmount * flatFee / vaultSharePrice + // + // NOTE: Round the flat fee calculation up to match the rounding used in + // the other flows. + uint256 timeRemaining = _calculateTimeRemaining(_maturityTime); + uint256 prepaidFlatFee = _bondAmount.mulDivUp( + _flatFee, + _vaultSharePrice + ); + + // 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 + // + // NOTE: Round the flat fee calculation up and the governance fee + // calculation down to match the rounding used in the other flows. + uint256 governanceFee = 2 * + _bondAmount.mulUp(_flatFee).mulDivDown( + _governanceLPFee, + _vaultSharePrice + ); + + // 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) { + prepaidFlatFee = prepaidFlatFee.mulDivDown( + 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 + // since this was pre-paid by the short and needs to be refunded. + // 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 + + // prepaidFlatFee - + // flatFee - + // governancFee + // + // 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 bondAmount = _bondAmount; // avoid stack-too-deep + uint256 vaultSharePrice = _vaultSharePrice; // avoid stack-too-deep + uint256 shareProceeds = bondAmount + .mulDivDown(closeVaultSharePrice, openVaultSharePrice) + .divDown(vaultSharePrice) + + prepaidFlatFee - + flatFee - + governanceFee; + + return (shareProceeds, flatFee, governanceFee); + } +} 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( 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 = ["*"] 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", ] diff --git a/test/instances/aave/AaveHyperdrive.t.sol b/test/instances/aave/AaveHyperdrive.t.sol index f768a3602..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; @@ -75,6 +76,7 @@ contract AaveHyperdriveTest is InstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 10, roundTripLpInstantaneousWithBaseTolerance: 1e5, roundTripLpWithdrawalSharesWithBaseTolerance: 1e5, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -84,9 +86,14 @@ contract AaveHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 10, roundTripLpInstantaneousWithSharesTolerance: 1e5, roundTripLpWithdrawalSharesWithSharesTolerance: 1e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -96,6 +103,10 @@ contract AaveHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 @@ -224,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) @@ -269,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 12dcd93dc..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; @@ -85,6 +86,7 @@ contract AaveL2HyperdriveTest is InstanceTest { closeLongWithBaseTolerance: 20, closeShortWithBaseUpperBoundTolerance: 10, closeShortWithBaseTolerance: 100, + burnWithBaseTolerance: 10, roundTripLpInstantaneousWithBaseTolerance: 1e5, roundTripLpWithdrawalSharesWithBaseTolerance: 1e5, roundTripLongInstantaneousWithBaseUpperBoundTolerance: 1e3, @@ -94,9 +96,14 @@ contract AaveL2HyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e5, roundTripLpWithdrawalSharesWithSharesTolerance: 1e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -106,6 +113,10 @@ contract AaveL2HyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 2 @@ -226,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) @@ -240,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) @@ -254,7 +273,7 @@ contract AaveL2HyperdriveTest is InstanceTest { bytes32(uint256(reserveDataLocation) + 1), bytes32( (uint256(data.currentLiquidityRate) << 128) | - uint256(totalAmount) + uint256(reserveNormalizedIncome) ) ); vm.store( @@ -266,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 101ec2f73..208343de6 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,17 @@ contract AerodromeLp_AERO_USDC_Hyperdrive is AerodromeLpHyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 100, roundTripShortInstantaneousWithBaseTolerance: 10, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 100, + roundTripPairInstantaneousWithBaseTolerance: 10, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, + 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 +89,10 @@ contract AerodromeLp_AERO_USDC_Hyperdrive is AerodromeLpHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 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..9c0b5634c 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,14 @@ contract CbETHBaseTest is ChainlinkHyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e3, roundTripLpWithdrawalSharesWithSharesTolerance: 1e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e4, @@ -79,6 +85,10 @@ contract CbETHBaseTest is ChainlinkHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesUpperBoundTolerance: 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..734d7f98b 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,14 @@ contract WstETHGnosisChainTest is ChainlinkHyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e3, roundTripLpWithdrawalSharesWithSharesTolerance: 1e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -80,6 +86,10 @@ contract WstETHGnosisChainTest is ChainlinkHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesUpperBoundTolerance: 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..145754c59 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,17 @@ 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, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, + 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 +89,10 @@ contract Corn_LBTC_Hyperdrive is CornHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 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..e5867a8a7 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,17 @@ 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, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, + 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 +89,10 @@ contract Corn_sDAI_Hyperdrive is CornHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 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..5087e8a17 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,14 @@ contract EETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e5, roundTripLpWithdrawalSharesWithSharesTolerance: 1e5, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -100,6 +106,10 @@ contract EETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e4, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + 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..229d48fbf 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,14 @@ contract MoonwellETHHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -76,6 +82,10 @@ contract MoonwellETHHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + 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..b687fb1bf 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,14 @@ contract MoonwellEURCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e16, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -83,6 +89,10 @@ contract MoonwellEURCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + 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..53852769e 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,14 @@ contract MoonwellUSDCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + 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 +94,10 @@ contract MoonwellUSDCHyperdriveTest is MetaMorphoHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + 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..0ec364c35 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,14 @@ contract SUSDeHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e8, roundTripLpWithdrawalSharesWithSharesTolerance: 1e8, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -106,6 +112,10 @@ contract SUSDeHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e8, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, + 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..3a7c62533 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,14 @@ contract scrvUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -90,6 +96,10 @@ contract scrvUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + 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..edede9e6d 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,14 @@ contract SnARSHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e8, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -101,6 +107,10 @@ contract SnARSHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + 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..f8285ca8b 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,14 @@ contract stUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -93,6 +99,10 @@ contract stUSDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + 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..69eb41197 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,14 @@ contract sGYDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -86,6 +92,10 @@ contract sGYDHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + 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..ac5a75b02 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,14 @@ contract sGYD_gnosis_HyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -90,6 +96,10 @@ contract sGYD_gnosis_HyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + 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 50c1d2c21..6f716f2df 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, + roundTripPairInstantaneousWithBaseTolerance: 1e8, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 1e3, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 1e3, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -98,6 +104,10 @@ contract sUSDSHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e6, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + roundTripPairMaturityWithSharesTolerance: 1e5, // The verification tolerances. verifyDepositTolerance: 5, verifyWithdrawalTolerance: 2 @@ -120,19 +130,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/erc4626/sxDai.t.sol b/test/instances/erc4626/sxDai.t.sol index 789fbf0de..6bc9146f3 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,14 @@ contract sxDaiHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -86,6 +92,10 @@ contract sxDaiHyperdriveTest is ERC4626HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e6, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + 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 57e7ca2eb..627da2a30 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,14 @@ contract EzETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 1e6, closeShortWithSharesTolerance: 1e6, + burnWithSharesTolerance: 1e6, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -110,9 +116,13 @@ contract EzETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e7, roundTripShortMaturityWithSharesTolerance: 1e8, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e7, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + roundTripPairMaturityWithSharesTolerance: 1e8, // The verification tolerances. - verifyDepositTolerance: 2, - verifyWithdrawalTolerance: 1_000 + 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..c17c36763 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,14 @@ contract EzETHLineaHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 100, roundTripLpWithdrawalSharesWithSharesTolerance: 1e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -96,6 +102,10 @@ contract EzETHLineaHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, + 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..a44a99dae 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,14 @@ contract LsETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e3, roundTripLpWithdrawalSharesWithSharesTolerance: 1e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -98,6 +104,10 @@ contract LsETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, + 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..3984f3c8f 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, + roundTripPairInstantaneousWithBaseTolerance: 1e13, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + 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,10 @@ contract MorphoBlue_USDe_DAI_HyperdriveTest is roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 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..8d3962762 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,17 @@ 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, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, + 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 +95,10 @@ contract MorphoBlue_WBTC_USDC_Hyperdrive is MorphoBlueHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 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 52289c5bd..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 @@ -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,17 @@ 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, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, + 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 +97,10 @@ contract MorphoBlue_cbETH_USDC_Base_HyperdriveTest is roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 0, + roundTripPairMaturityWithSharesTolerance: 0, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 3 @@ -129,10 +139,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/instances/morpho-blue/MorphoBlue_cbETH_USDC_Mainnet_Hyperdrive.t.sol b/test/instances/morpho-blue/MorphoBlue_cbETH_USDC_Mainnet_Hyperdrive.t.sol index 52c0a2b91..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 @@ -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,17 @@ 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, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, + 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 +97,10 @@ contract MorphoBlue_cbBTC_USDC_Mainnet_HyperdriveTest is roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 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..092556f16 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, + roundTripPairInstantaneousWithBaseTolerance: 1e13, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + 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,10 @@ contract MorphoBlue_sUSDe_DAI_HyperdriveTest is roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 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..de44c4445 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, + roundTripPairInstantaneousWithBaseTolerance: 1e13, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + 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,10 @@ contract MorphoBlue_wstETH_USDA_HyperdriveTest is roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 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..b36e6e230 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,17 @@ 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, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, + 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 +97,10 @@ contract MorphoBlue_wstETH_USDC_HyperdriveTest is roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 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..928ce9f1b 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,14 @@ contract RETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e3, roundTripShortMaturityWithBaseTolerance: 1e3, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e3, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 2e3, roundTripLpWithdrawalSharesWithSharesTolerance: 2e3, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -98,6 +104,10 @@ contract RETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e4, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, + 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..41cfbee6e 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,14 @@ contract RsETHLineaHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 100, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 100, roundTripLpInstantaneousWithSharesTolerance: 100, roundTripLpWithdrawalSharesWithSharesTolerance: 1e4, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -97,6 +103,10 @@ contract RsETHLineaHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e4, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, + 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..94017b3b0 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,14 @@ contract SavingsUSDS_L2_Base_Hyperdrive is SavingsUSDSL2HyperdriveInstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e5, roundTripShortMaturityWithBaseTolerance: 1e5, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e5, + roundTripPairMaturityWithBaseUpperBoundTolerance: 1e3, + roundTripPairMaturityWithBaseTolerance: 1e5, // The share test tolerances. closeLongWithSharesTolerance: 1e3, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 1e3, roundTripLpInstantaneousWithSharesTolerance: 1e7, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -84,6 +90,10 @@ contract SavingsUSDS_L2_Base_Hyperdrive is SavingsUSDSL2HyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e5, roundTripShortMaturityWithSharesTolerance: 1e5, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 1e3, + 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..97fd26234 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,17 @@ 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, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, + 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 +89,10 @@ contract StakingUSDS_Chronicle_Hyperdrive is StakingUSDSHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 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..4ca77cc66 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,17 @@ 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, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, + 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 +89,10 @@ contract StakingUSDS_Sky_Hyperdrive is StakingUSDSHyperdriveInstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 0, roundTripShortInstantaneousWithSharesTolerance: 0, roundTripShortMaturityWithSharesTolerance: 0, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 0, + roundTripPairInstantaneousWithSharesTolerance: 0, + roundTripPairMaturityWithSharesUpperBoundTolerance: 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..aff424b94 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,14 @@ contract StETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 0, roundTripShortInstantaneousWithBaseTolerance: 0, roundTripShortMaturityWithBaseTolerance: 0, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 0, + roundTripPairInstantaneousWithBaseTolerance: 0, + roundTripPairMaturityWithBaseUpperBoundTolerance: 0, + roundTripPairMaturityWithBaseTolerance: 0, // The share test tolerances. closeLongWithSharesTolerance: 20, closeShortWithSharesTolerance: 100, + burnWithSharesTolerance: 20, roundTripLpInstantaneousWithSharesTolerance: 1e5, roundTripLpWithdrawalSharesWithSharesTolerance: 1e5, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -94,6 +100,10 @@ contract StETHHyperdriveTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e5, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, + 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..bd85772b1 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,14 @@ contract StkWellHyperdriveInstanceTest is InstanceTest { roundTripShortInstantaneousWithBaseUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithBaseTolerance: 1e3, roundTripShortMaturityWithBaseTolerance: 1e3, + roundTripPairInstantaneousWithBaseUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithBaseTolerance: 1e3, + roundTripPairMaturityWithBaseUpperBoundTolerance: 100, + roundTripPairMaturityWithBaseTolerance: 1e3, // The share test tolerances. closeLongWithSharesTolerance: 2, closeShortWithSharesTolerance: 2, + burnWithSharesTolerance: 2, roundTripLpInstantaneousWithSharesTolerance: 1e3, roundTripLpWithdrawalSharesWithSharesTolerance: 1e7, roundTripLongInstantaneousWithSharesUpperBoundTolerance: 1e3, @@ -99,6 +105,10 @@ contract StkWellHyperdriveInstanceTest is InstanceTest { roundTripShortInstantaneousWithSharesUpperBoundTolerance: 1e3, roundTripShortInstantaneousWithSharesTolerance: 1e3, roundTripShortMaturityWithSharesTolerance: 1e3, + roundTripPairInstantaneousWithSharesUpperBoundTolerance: 1e3, + roundTripPairInstantaneousWithSharesTolerance: 1e3, + roundTripPairMaturityWithSharesUpperBoundTolerance: 100, + roundTripPairMaturityWithSharesTolerance: 1e3, // The verification tolerances. verifyDepositTolerance: 2, verifyWithdrawalTolerance: 3 diff --git a/test/integrations/hyperdrive/BurnTest.t.sol b/test/integrations/hyperdrive/BurnTest.t.sol new file mode 100644 index 000000000..20695e7c4 --- /dev/null +++ b/test/integrations/hyperdrive/BurnTest.t.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.24; + +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))); + } +} 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..08a4fd2be 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); + 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 + // 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); + 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. + 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/MintTest.t.sol b/test/integrations/hyperdrive/MintTest.t.sol new file mode 100644 index 000000000..c69182833 --- /dev/null +++ b/test/integrations/hyperdrive/MintTest.t.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.24; + +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) + ), + 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 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))); + } +} 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); 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 + ); + } + } } diff --git a/test/units/hyperdrive/BurnTest.t.sol b/test/units/hyperdrive/BurnTest.t.sol new file mode 100644 index 000000000..14a0e531a --- /dev/null +++ b/test/units/hyperdrive/BurnTest.t.sol @@ -0,0 +1,676 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.24; + +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); + } + + /// @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); + } + + /// @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. + 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 + 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 closeVaultSharePrice = block.timestamp < _testCase.maturityTime + ? hyperdrive.getPoolInfo().vaultSharePrice + : hyperdrive.getCheckpoint(_testCase.maturityTime).vaultSharePrice; + uint256 governanceFeeAdjustment = 2 * + _testCase + .bondAmount + .mulUp(hyperdrive.getPoolConfig().fees.flat) + .mulDown( + hyperdrive.calculateTimeRemaining(_testCase.maturityTime) + ) + .mulDown(hyperdrive.getPoolConfig().fees.governanceLP); + if (closeVaultSharePrice < openVaultSharePrice) { + governanceFeeAdjustment = governanceFeeAdjustment.mulDivDown( + closeVaultSharePrice, + openVaultSharePrice + ); + } + 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( + 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. If negative interest accrued, this fee needs to be + // scaled down in proportion to the negative interest. + uint256 flatFee = 2 * + _testCase + .bondAmount + .mulUp(hyperdrive.getPoolConfig().fees.flat) + .mulDown( + ONE - + hyperdrive.calculateTimeRemaining( + _testCase.maturityTime + ) + ) + .mulDown(ONE - hyperdrive.getPoolConfig().fees.governanceLP); + if (closeVaultSharePrice < openVaultSharePrice) { + flatFee = flatFee.mulDivDown( + closeVaultSharePrice, + openVaultSharePrice + ); + } + 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 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 + ); + assertEq( + 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 + ); + } + assertApproxEqAbs( + hyperdrive.getUncollectedGovernanceFees(), + _testCase.governanceFeesAccruedBefore + governanceFee, + 1 + ); + + // 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/units/hyperdrive/MintTest.t.sol b/test/units/hyperdrive/MintTest.t.sol new file mode 100644 index 000000000..0835f157b --- /dev/null +++ b/test/units/hyperdrive/MintTest.t.sol @@ -0,0 +1,515 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.24; + +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 } 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 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(); + + // 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); + } + + /// @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, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } + + /// @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, + 0, + minVaultSharePrice, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } + + /// @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(); + 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, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } + + /// @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, + 0, + IHyperdrive.PairOptions({ + longDestination: address(0), + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + } + + /// @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, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: address(0), + asBase: true, + extraData: new bytes(0) + }) + ); + } + + /// @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, + 0, + IHyperdrive.PairOptions({ + longDestination: bob, + shortDestination: bob, + asBase: true, + extraData: new bytes(0) + }) + ); + pause(false); + } + + /// @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. + MintTestCase memory testCase = _mintTestCase( + alice, // funder + bob, // long + celine, // short + baseAmount, // amount + true, // asBase + "" // extraData + ); + + // Verify the mint transaction. + _verifyMint(testCase); + } + + /// @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(); + 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 + ); + + // Verify the mint transaction. + _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; + 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( + _testCase.amount, + 0, + 0, + IHyperdrive.PairOptions({ + longDestination: _testCase.long, + shortDestination: _testCase.short, + asBase: _testCase.asBase, + extraData: _testCase.extraData + }) + ); + assertEq(maturityTime, _testCase.maturityTime); + + // Verify that the balances increased and decreased by the right amounts. + assertEq( + baseToken.balanceOf(_testCase.funder), + _testCase.funderBaseBalanceBefore - _testCase.amount + ); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + _testCase.maturityTime + ), + _testCase.long + ), + _testCase.longBalanceBefore + bondAmount + ); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + _testCase.maturityTime + ), + _testCase.short + ), + _testCase.shortBalanceBefore + bondAmount + ); + assertEq( + baseToken.balanceOf(address(hyperdrive)), + _testCase.hyperdriveBaseBalanceBefore + _testCase.amount + ); + + // Verify that idle, spot price, LP share price, and pool depth are all + // unchanged. + assertEq(hyperdrive.idle(), _testCase.idleBefore); + assertEq(hyperdrive.calculateSpotPrice(), _testCase.spotPriceBefore); + assertEq(hyperdrive.k(), _testCase.kBefore); + assertEq( + hyperdrive.getPoolInfo().lpSharePrice, + _testCase.lpSharePriceBefore + ); + + // 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(), + _testCase.governanceFeesAccruedBefore + + 2 * + bondAmount + .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 + // 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.min( + hyperdrive.getPoolInfo().vaultSharePrice + ), + openVaultSharePrice + ) + + bondAmount.mulDown(hyperdrive.getPoolConfig().fees.flat) + + 2 * + bondAmount.mulDown(hyperdrive.getPoolConfig().fees.flat).mulDown( + hyperdrive.getPoolConfig().fees.governanceLP + ); + assertGt(_testCase.amount, requiredBaseAmount); + assertApproxEqAbs(_testCase.amount, requiredBaseAmount, 1e6); + + // Verify the `Mint` event. + _verifyMintEvent(_testCase, bondAmount); + } + + /// @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); + } +} 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"; diff --git a/test/units/libraries/FixedPointMath.t.sol b/test/units/libraries/FixedPointMath.t.sol index 4b6906fce..9a9655c22 100644 --- a/test/units/libraries/FixedPointMath.t.sol +++ b/test/units/libraries/FixedPointMath.t.sol @@ -301,9 +301,14 @@ contract FixedPointMathTest is Test { y = y.normalizeToRange(0, 1); // NOTE: Coverage only works if I initialize the fixture in the test function MockFixedPointMath mockFixedPointMath = new MockFixedPointMath(); - uint256 result = mockFixedPointMath.pow(x, y); - uint256 expected = LogExpMath.pow(x, y); - assertApproxEqAbs(result, expected, 1e5 wei); + try mockFixedPointMath.pow(x, y) returns (uint256 result) { + uint256 expected = LogExpMath.pow(x, y); + assertApproxEqAbs(result, expected, 1e5 wei); + } catch (bytes memory error) { + // NOTE: This function infrequently fails due to an improper cast. + // Since this is fuzzing over a large range, this is okay. + assertEq(bytes4(error), IHyperdrive.UnsafeCastToInt256.selector); + } } /// @dev This test is to check that the pow function returns 1e18 when the exponent is 0 diff --git a/test/utils/HyperdriveTest.sol b/test/utils/HyperdriveTest.sol index c616032c9..164569c17 100644 --- a/test/utils/HyperdriveTest.sol +++ b/test/utils/HyperdriveTest.sol @@ -859,6 +859,153 @@ 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.minSlippage, // min output + overrides.minSharePrice, // min vault share price + IHyperdrive.PairOptions({ + 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.minSlippage, // min output + overrides.minSharePrice, // min vault share price + IHyperdrive.PairOptions({ + 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 + minSlippage: 0, // min output of 0 + 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 + minSlippage: 0, // min output of 0 + maxSlippage: type(uint256).max, // unused + extraData: new bytes(0) // unused + }) + ); + } + + 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 { diff --git a/test/utils/InstanceTest.sol b/test/utils/InstanceTest.sol index 649ad011c..2129617c8 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,30 @@ 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 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; /// @dev The equality tolerance in wei for `verifyDeposit`. uint256 verifyDepositTolerance; /// @dev The equality tolerance in wei for `verifyWithdrawal`. @@ -3060,6 +3090,1060 @@ 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) + }) + ); + } + } + + /// @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 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); + + // Accrue interest for a term. + if (config.shouldAccrueInterest) { + advanceTime( + hyperdrive.getPoolConfig().positionDuration, + int256(FIXED_RATE) + ); + } else { + advanceTime(hyperdrive.getPoolConfig().positionDuration, 0); + } + + // 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 + ); + + // Bob mints bonds with the share token. + (uint256 maturityTime, uint256 bondAmount) = mint( + bob, + sharesPaid, + 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 burning the bonds. + ( + 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); + + // 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.burnWithSharesTolerance + ); + + // Ensure the withdrawal accounting is correct. + AccountBalances memory bobBalancesBefore_ = bobBalancesBefore; // avoid stack-too-deep + verifyWithdrawal( + bob, + baseProceeds, + false, + totalBaseSupplyBefore, + totalShareSupplyBefore, + bobBalancesBefore_, + hyperdriveBalancesBefore + ); + } + + /// @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); + + // 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; + } + + // We normalize the basePaid variable within a valid range the market + // can support. + basePaid = basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + maxBaseAmount + ); + + // 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; + + // 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); + + // Bob mints bonds with the share token. + (maturityTime, bondAmount) = mint( + bob, + sharesPaid.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 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) + }) + ); + + // Early termination if base withdraws are not supported. + if (!config.enableBaseWithdraws) { + return; + } + + // 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.burnWithBaseTolerance + ); + + // Ensure the withdrawal accounting is correct. + verifyWithdrawal( + bob, + baseProceeds, + true, + totalBaseSupplyBefore, + totalShareSupplyBefore, + bobBalancesBefore, + hyperdriveBalancesBefore + ); + } + + /// @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; + } + + // 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; + } + + // Bob mints some bonds with base. + _basePaid = _basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + maxBaseAmount + ); + (uint256 maturityTime, uint256 bondAmount) = mint(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 burns his bonds with base as the target asset. + baseProceeds = burn(bob, maturityTime, bondAmount); + + // 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); + + // 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 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; + } + + // 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 + ); + + // 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 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); + + // 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 + ); + } + } + + // 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 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; + } + + // 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; + } + + // Bob mints some bonds with base. + _basePaid = _basePaid.normalizeToRange( + 2 * hyperdrive.getPoolConfig().minimumTransactionAmount, + maxBaseAmount + ); + (uint256 maturityTime, uint256 bondAmount) = mint(bob, _basePaid); + + // 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)); + + // 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 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 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 /// function test_sweep_failure_directSweep() external {