From c062145ab3100ef2b46c347fe7d3245e8512f701 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Wed, 23 Oct 2024 11:43:38 -0500 Subject: [PATCH 01/16] first stab at partial closures --- contracts/Everlong.sol | 18 ++++++++++++++++++ contracts/libraries/HyperdriveExecution.sol | 1 + 2 files changed, 19 insertions(+) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index cb168ce..b5e9bf6 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.22; +import { console2 as console } from "forge-std/console2.sol"; import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol"; import { SafeCast } from "hyperdrive/contracts/src/libraries/SafeCast.sol"; @@ -422,6 +423,23 @@ contract Everlong is IEverlong { } } + ///// @dev Close positions until the targeted amount of output is received. + ///// @param _targetOutput Minimum amount of proceeds to receive. + ///// @return output Total output received from closed positions. + //function _closePositions( + // uint256 _targetOutput + //) internal returns (uint256 output) { + // while (!_portfolio.isEmpty() && output < _targetOutput) { + // output += IHyperdrive(hyperdrive).closeLong( + // asBase, + // _portfolio.head(), + // "" + // ); + // _portfolio.handleClosePosition(); + // } + // return output; + //} + /// @dev Close positions until the targeted amount of output is received. /// @param _targetOutput Target amount of proceeds to receive. /// @return output Total output received from closed positions. diff --git a/contracts/libraries/HyperdriveExecution.sol b/contracts/libraries/HyperdriveExecution.sol index 657fe71..72e1beb 100644 --- a/contracts/libraries/HyperdriveExecution.sol +++ b/contracts/libraries/HyperdriveExecution.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; +import { console2 as console } from "forge-std/console2.sol"; import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol"; import { HyperdriveMath } from "hyperdrive/contracts/src/libraries/HyperdriveMath.sol"; From 4d6b9826852cf80d4dbccb776ff115ae20f8d707 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Wed, 23 Oct 2024 16:47:17 -0500 Subject: [PATCH 02/16] introduce minimum remaining bonds threshold to avoid positions with very few bonds --- contracts/Everlong.sol | 5 +++++ contracts/libraries/Portfolio.sol | 1 + test/integration/Sandwich.t.sol | 2 ++ 3 files changed, 8 insertions(+) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index b5e9bf6..b662942 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -522,6 +522,11 @@ contract Everlong is IEverlong { // Update portfolio accounting to include the partial closure. _portfolio.handleClosePosition(); + if (bondsNeeded <= uint256(position.bondAmount)) { + return output; + } else { + bondsNeeded -= uint256(position.bondAmount); + } } } diff --git a/contracts/libraries/Portfolio.sol b/contracts/libraries/Portfolio.sol index b59ee05..3773148 100644 --- a/contracts/libraries/Portfolio.sol +++ b/contracts/libraries/Portfolio.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; +import { console2 as console } from "forge-std/console2.sol"; import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol"; import { SafeCast } from "hyperdrive/contracts/src/libraries/SafeCast.sol"; import { IEverlong } from "../interfaces/IEverlong.sol"; diff --git a/test/integration/Sandwich.t.sol b/test/integration/Sandwich.t.sol index f3dfdb9..e06e579 100644 --- a/test/integration/Sandwich.t.sol +++ b/test/integration/Sandwich.t.sol @@ -210,6 +210,8 @@ contract Sandwich is EverlongTest { attacker ); + everlong.rebalance(); + // The bystander redeems from Everlong. // // While not needed for the assertion below, it's included to ensure From bc3b665e51b7e56d8121d5bdf78b114909bb1dec Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Mon, 28 Oct 2024 16:19:22 -0500 Subject: [PATCH 03/16] fix --- test/integration/Sandwich.t.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/Sandwich.t.sol b/test/integration/Sandwich.t.sol index e06e579..f3dfdb9 100644 --- a/test/integration/Sandwich.t.sol +++ b/test/integration/Sandwich.t.sol @@ -210,8 +210,6 @@ contract Sandwich is EverlongTest { attacker ); - everlong.rebalance(); - // The bystander redeems from Everlong. // // While not needed for the assertion below, it's included to ensure From 0a8c8d91123799129ff31267e45d525ed08c3337 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Tue, 29 Oct 2024 07:31:52 -0500 Subject: [PATCH 04/16] better partial estimation, fuzz tests, pricing edge case test --- contracts/Everlong.sol | 28 ++++----------------- contracts/libraries/HyperdriveExecution.sol | 1 - contracts/libraries/Portfolio.sol | 1 - 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index b662942..5440250 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.22; -import { console2 as console } from "forge-std/console2.sol"; import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol"; import { SafeCast } from "hyperdrive/contracts/src/libraries/SafeCast.sol"; @@ -273,6 +272,9 @@ contract Everlong is IEverlong { // /// @dev Attempt rebalancing after a deposit if idle is above max. function _afterDeposit(uint256 _assets, uint256) internal virtual override { + // Update `_totalAssets` to include the deposit. + _totalAssets += _assets; + // If there is excess liquidity beyond the max, rebalance. if (ERC20(_asset).balanceOf(address(this)) > maxIdleLiquidity()) { rebalance(); @@ -404,6 +406,8 @@ contract Everlong is IEverlong { // │ Hyperdrive │ // ╰─────────────────────────────────────────────────────────╯ + // TODO: Decide if we want to put a slippage guard here. + // /// @dev Close only matured positions in the portfolio. /// @return output Proceeds of closing the matured positions. function _closeMaturedPositions() internal returns (uint256 output) { @@ -423,23 +427,6 @@ contract Everlong is IEverlong { } } - ///// @dev Close positions until the targeted amount of output is received. - ///// @param _targetOutput Minimum amount of proceeds to receive. - ///// @return output Total output received from closed positions. - //function _closePositions( - // uint256 _targetOutput - //) internal returns (uint256 output) { - // while (!_portfolio.isEmpty() && output < _targetOutput) { - // output += IHyperdrive(hyperdrive).closeLong( - // asBase, - // _portfolio.head(), - // "" - // ); - // _portfolio.handleClosePosition(); - // } - // return output; - //} - /// @dev Close positions until the targeted amount of output is received. /// @param _targetOutput Target amount of proceeds to receive. /// @return output Total output received from closed positions. @@ -522,11 +509,6 @@ contract Everlong is IEverlong { // Update portfolio accounting to include the partial closure. _portfolio.handleClosePosition(); - if (bondsNeeded <= uint256(position.bondAmount)) { - return output; - } else { - bondsNeeded -= uint256(position.bondAmount); - } } } diff --git a/contracts/libraries/HyperdriveExecution.sol b/contracts/libraries/HyperdriveExecution.sol index 72e1beb..657fe71 100644 --- a/contracts/libraries/HyperdriveExecution.sol +++ b/contracts/libraries/HyperdriveExecution.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; -import { console2 as console } from "forge-std/console2.sol"; import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol"; import { HyperdriveMath } from "hyperdrive/contracts/src/libraries/HyperdriveMath.sol"; diff --git a/contracts/libraries/Portfolio.sol b/contracts/libraries/Portfolio.sol index 3773148..b59ee05 100644 --- a/contracts/libraries/Portfolio.sol +++ b/contracts/libraries/Portfolio.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; -import { console2 as console } from "forge-std/console2.sol"; import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol"; import { SafeCast } from "hyperdrive/contracts/src/libraries/SafeCast.sol"; import { IEverlong } from "../interfaces/IEverlong.sol"; From 84af483a7cccea2021785f0d6050e416eb45d63b Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Tue, 29 Oct 2024 07:39:45 -0500 Subject: [PATCH 05/16] better comments, tighter tolerances --- test/integration/PartialClosures.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/PartialClosures.t.sol b/test/integration/PartialClosures.t.sol index ac18196..f6fd0dd 100644 --- a/test/integration/PartialClosures.t.sol +++ b/test/integration/PartialClosures.t.sol @@ -64,7 +64,7 @@ contract PartialClosures is EverlongTest { 1e18 - TARGET_IDLE_LIQUIDITY_PERCENTAGE ), positionBondsAfterRedeem, - 0.01e18 + 0.001e18 ); } From 321ec0b52fd4002ffad8b8a949f7fc84e993cb1f Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Tue, 29 Oct 2024 09:14:07 -0500 Subject: [PATCH 06/16] fixes to avoid underflow when trying to withdraw assets greater than everlong's _totalAssets --- contracts/Everlong.sol | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index 5440250..f9a364c 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.22; +import { console2 as console } from "forge-std/console2.sol"; import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol"; import { SafeCast } from "hyperdrive/contracts/src/libraries/SafeCast.sol"; @@ -272,9 +273,6 @@ contract Everlong is IEverlong { // /// @dev Attempt rebalancing after a deposit if idle is above max. function _afterDeposit(uint256 _assets, uint256) internal virtual override { - // Update `_totalAssets` to include the deposit. - _totalAssets += _assets; - // If there is excess liquidity beyond the max, rebalance. if (ERC20(_asset).balanceOf(address(this)) > maxIdleLiquidity()) { rebalance(); @@ -521,19 +519,30 @@ contract Everlong is IEverlong { /// @return value The present portfolio value. function _calculateTotalAssets() internal view returns (uint256 value) { value = ERC20(_asset).balanceOf(address(this)); + // uint256 i; + // IEverlong.Position memory position; + // while (i < _portfolio.positionCount()) { + // position = _portfolio.at(i); + // value += IHyperdrive(hyperdrive) + // .previewCloseLong(asBase, position, "") + // .mulDown(1e18 - maxCloseLongSlippage); + // i++; + // } if (_portfolio.totalBonds != 0) { // NOTE: The maturity time is rounded to the next checkpoint to // underestimate the portfolio value. - value += IHyperdrive(hyperdrive).previewCloseLong( - asBase, - IEverlong.Position({ - maturityTime: IHyperdrive(hyperdrive) - .getCheckpointIdUp(_portfolio.avgMaturityTime) - .toUint128(), - bondAmount: _portfolio.totalBonds - }), - "" - ); + value += IHyperdrive(hyperdrive) + .previewCloseLong( + asBase, + IEverlong.Position({ + maturityTime: IHyperdrive(hyperdrive) + .getCheckpointIdUp(_portfolio.avgMaturityTime) + .toUint128(), + bondAmount: _portfolio.totalBonds + }), + "" + ) + .mulDown(1e18 - maxCloseLongSlippage); } } From 80658ee01c166eec8e22fae0bb2adc08f05a1e49 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Tue, 29 Oct 2024 09:14:35 -0500 Subject: [PATCH 07/16] cleanup --- contracts/Everlong.sol | 9 --------- 1 file changed, 9 deletions(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index f9a364c..14ac6b2 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -519,15 +519,6 @@ contract Everlong is IEverlong { /// @return value The present portfolio value. function _calculateTotalAssets() internal view returns (uint256 value) { value = ERC20(_asset).balanceOf(address(this)); - // uint256 i; - // IEverlong.Position memory position; - // while (i < _portfolio.positionCount()) { - // position = _portfolio.at(i); - // value += IHyperdrive(hyperdrive) - // .previewCloseLong(asBase, position, "") - // .mulDown(1e18 - maxCloseLongSlippage); - // i++; - // } if (_portfolio.totalBonds != 0) { // NOTE: The maturity time is rounded to the next checkpoint to // underestimate the portfolio value. From d9ed657e622f4d4f894a1371e1e9f0dfa51fa969 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Tue, 29 Oct 2024 09:48:45 -0500 Subject: [PATCH 08/16] remove console --- contracts/Everlong.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index 14ac6b2..a2fc0fe 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.22; -import { console2 as console } from "forge-std/console2.sol"; import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol"; import { SafeCast } from "hyperdrive/contracts/src/libraries/SafeCast.sol"; From dbbbc0f39aa71292dc6e2226ace088ac36c32d4b Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Thu, 31 Oct 2024 10:53:35 -0500 Subject: [PATCH 09/16] no need for mature position slippage guards --- contracts/Everlong.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index a2fc0fe..87e0d8a 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -403,8 +403,6 @@ contract Everlong is IEverlong { // │ Hyperdrive │ // ╰─────────────────────────────────────────────────────────╯ - // TODO: Decide if we want to put a slippage guard here. - // /// @dev Close only matured positions in the portfolio. /// @return output Proceeds of closing the matured positions. function _closeMaturedPositions() internal returns (uint256 output) { From 3761bb46ff2e2a8bca296022d23b01589b3116cb Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Thu, 31 Oct 2024 15:06:35 -0500 Subject: [PATCH 10/16] slippage guard docs and partial closure test adjustments --- contracts/Everlong.sol | 22 ++++++++++------------ test/integration/PartialClosures.t.sol | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index 87e0d8a..cb168ce 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -519,18 +519,16 @@ contract Everlong is IEverlong { if (_portfolio.totalBonds != 0) { // NOTE: The maturity time is rounded to the next checkpoint to // underestimate the portfolio value. - value += IHyperdrive(hyperdrive) - .previewCloseLong( - asBase, - IEverlong.Position({ - maturityTime: IHyperdrive(hyperdrive) - .getCheckpointIdUp(_portfolio.avgMaturityTime) - .toUint128(), - bondAmount: _portfolio.totalBonds - }), - "" - ) - .mulDown(1e18 - maxCloseLongSlippage); + value += IHyperdrive(hyperdrive).previewCloseLong( + asBase, + IEverlong.Position({ + maturityTime: IHyperdrive(hyperdrive) + .getCheckpointIdUp(_portfolio.avgMaturityTime) + .toUint128(), + bondAmount: _portfolio.totalBonds + }), + "" + ); } } diff --git a/test/integration/PartialClosures.t.sol b/test/integration/PartialClosures.t.sol index f6fd0dd..ac18196 100644 --- a/test/integration/PartialClosures.t.sol +++ b/test/integration/PartialClosures.t.sol @@ -64,7 +64,7 @@ contract PartialClosures is EverlongTest { 1e18 - TARGET_IDLE_LIQUIDITY_PERCENTAGE ), positionBondsAfterRedeem, - 0.001e18 + 0.01e18 ); } From f940f8537640ce9a5a6ae4b5083e0a38ea620220 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Tue, 29 Oct 2024 22:25:58 -0500 Subject: [PATCH 11/16] convert rebalance to only be called by an external keeper Keepers are expected to use the configuration object to circumvent hyperdrive errors --- contracts/Everlong.sol | 163 ++++++++++---- contracts/interfaces/IEverlong.sol | 19 ++ contracts/interfaces/IEverlongPortfolio.sol | 16 ++ contracts/libraries/HyperdriveExecution.sol | 34 ++- test/harnesses/EverlongTest.sol | 202 +++++++++++++++++- test/integration/CloseImmatureLongs.t.sol | 6 +- test/integration/PartialClosures.t.sol | 7 +- .../VaultSharePriceManipulation.t.sol | 12 +- test/units/EverlongERC4626.t.sol | 4 +- test/units/EverlongPortfolio.t.sol | 151 ++++++++++++- test/units/HyperdriveExecution.t.sol | 6 +- 11 files changed, 549 insertions(+), 71 deletions(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index cb168ce..bdd051e 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.22; import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol"; import { SafeCast } from "hyperdrive/contracts/src/libraries/SafeCast.sol"; -import { ERC20 } from "openzeppelin/token/ERC20/ERC20.sol"; +import { IERC20 } from "openzeppelin/interfaces/IERC20.sol"; import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol"; import { IEverlong } from "./interfaces/IEverlong.sol"; @@ -74,7 +74,7 @@ contract Everlong is IEverlong { using HyperdriveExecutionLibrary for IHyperdrive; using Portfolio for Portfolio.State; using SafeCast for *; - using SafeERC20 for ERC20; + using SafeERC20 for IERC20; // ╭─────────────────────────────────────────────────────────╮ // │ Storage │ @@ -267,17 +267,10 @@ contract Everlong is IEverlong { } } - // TODO: Do not rebalance on deposit. This change will require updating - // the test suite as well to perform rebalances when time is advanced. - // /// @dev Attempt rebalancing after a deposit if idle is above max. function _afterDeposit(uint256 _assets, uint256) internal virtual override { - // If there is excess liquidity beyond the max, rebalance. - if (ERC20(_asset).balanceOf(address(this)) > maxIdleLiquidity()) { - rebalance(); - } else { - _totalAssets += _assets; - } + // Add the deposit to Everlong's assets. + _totalAssets += _assets; } /// @dev Frees sufficient assets for a withdrawal by closing positions. @@ -298,8 +291,8 @@ contract Everlong is IEverlong { // // If we do not have enough balance to service the withdrawal after // closing any matured positions, close more positions. - uint256 balance = ERC20(_asset).balanceOf(address(this)) + - _closeMaturedPositions(); + uint256 balance = IERC20(_asset).balanceOf(address(this)) + + _closeMaturedPositions(type(uint256).max); if (_assets > balance) { _closePositions(_assets - balance); } @@ -313,35 +306,99 @@ contract Everlong is IEverlong { // │ Rebalancing │ // ╰─────────────────────────────────────────────────────────╯ - // TODO: Handle case where rebalancing would exceed gas limit - // TODO: Handle when Hyperdrive has insufficient liquidity. + // NOTE: Errors from hyperdrive are not handled. The keeper must configure + // the correct parameters to avoid issues with insufficient liquidity + // and running out of gas from mature position closures. + // + /// @notice Rebalance the everlong portfolio by closing mature positions + /// and using the proceeds over target idle to open new positions. + function rebalance() external onlyAdmin { + _rebalance( + IEverlong.RebalanceOptions({ + spendingOverride: 0, + minOutput: 0, + minVaultSharePrice: 0, + positionClosureLimit: type(uint256).max, + extraData: "" + }) + ); + } + + // NOTE: Errors from hyperdrive are not handled. The keeper must configure + // the correct parameters to avoid issues with insufficient liquidity + // and running out of gas from mature position closures. // /// @notice Rebalance the everlong portfolio by closing mature positions /// and using the proceeds over target idle to open new positions. - function rebalance() public override { + /// @param _options Options to control the rebalance behavior. + function rebalance( + IEverlong.RebalanceOptions memory _options + ) external onlyAdmin { + _rebalance(_options); + } + + // NOTE: Errors from hyperdrive are not handled. The keeper must configure + // the correct parameters to avoid issues with insufficient liquidity + // and running out of gas from mature position closures. + // + /// @dev Rebalance the everlong portfolio by closing mature positions + /// and using the proceeds over target idle to open new positions. + /// @param _options Options to control the rebalance behavior. + function _rebalance(IEverlong.RebalanceOptions memory _options) internal { // Early return if no rebalancing is needed. if (!canRebalance()) { return; } - // Calculate the new portfolio value and save it. - _totalAssets = _calculateTotalAssets(); - // Close matured positions. - _closeMaturedPositions(); + _closeMaturedPositions(_options.positionClosureLimit); + + // If Everlong has sufficient idle, open a new position. + if (canOpenPosition()) { + // Calculate how much idle to spend on the position. + uint256 balance = IERC20(_asset).balanceOf(address(this)); + uint256 target = targetIdleLiquidity(); + uint256 toSpend = balance - target; + if (_options.spendingOverride != 0) { + // Spending override must not be less than Hyperdrive's + // minimum transaction amount. + if ( + _options.spendingOverride < + IHyperdrive(hyperdrive) + .getPoolConfig() + .minimumTransactionAmount + ) { + revert SpendingOverrideTooLow(); + } + + // Spending override must not be greater than the difference + // between Everlong's balance and the max idle liquidity. + if (_options.spendingOverride > toSpend) { + revert SpendingOverrideTooHigh(); + } + + // Apply the spending override. + toSpend = _options.spendingOverride; + } - // Amount to spend is the current balance less the target idle. - uint256 toSpend = ERC20(_asset).balanceOf(address(this)) - - targetIdleLiquidity(); + // Open a new position. Leave an extra wei for the approval to keep + // the slot warm. + IERC20(_asset).forceApprove(address(hyperdrive), toSpend + 1); + (uint256 maturityTime, uint256 bondAmount) = IHyperdrive(hyperdrive) + .openLong( + asBase, + toSpend, + _options.minOutput, + _options.minVaultSharePrice, + _options.extraData + ); - // Open a new position. Leave an extra wei for the approval to keep - // the slot warm. - ERC20(_asset).forceApprove(address(hyperdrive), toSpend + 1); - (uint256 maturityTime, uint256 bondAmount) = IHyperdrive(hyperdrive) - .openLong(asBase, toSpend, ""); + // Account for the new position in the portfolio. + _portfolio.handleOpenPosition(maturityTime, bondAmount); + } - // Account for the new position in the portfolio. - _portfolio.handleOpenPosition(maturityTime, bondAmount); + // Calculate an updated portfolio value and save it. + _totalAssets = _calculateTotalAssets(); emit Rebalanced(); } @@ -354,14 +411,21 @@ contract Everlong is IEverlong { /// - The current idle liquidity is above the target. /// @return True if the portfolio can be rebalanced, false otherwise. function canRebalance() public view returns (bool) { - uint256 balance = ERC20(_asset).balanceOf(address(this)); - uint256 target = targetIdleLiquidity(); - return (hasMaturedPositions() || - (balance > target && - balance - target > + return hasMaturedPositions() || canOpenPosition(); + } + + /// @notice Returns whether Everlong has sufficient idle liquidity to open + /// a new position. + /// @return True if a new position can be opened, false otherwise. + function canOpenPosition() public view returns (bool) { + uint256 balance = IERC20(_asset).balanceOf(address(this)); + uint256 max = maxIdleLiquidity(); + return + balance > max && + (balance - max > IHyperdrive(hyperdrive) .getPoolConfig() - .minimumTransactionAmount)); + .minimumTransactionAmount); } /// @notice Returns the target amount of funds to keep idle in Everlong. @@ -404,21 +468,42 @@ contract Everlong is IEverlong { // ╰─────────────────────────────────────────────────────────╯ /// @dev Close only matured positions in the portfolio. + /// @param _limit The maximum number of positions to close. /// @return output Proceeds of closing the matured positions. - function _closeMaturedPositions() internal returns (uint256 output) { + function _closeMaturedPositions( + uint256 _limit + ) internal returns (uint256 output) { + // Iterate through positions from most to least mature. + // Exit if: + // - There are no more positions. + // - The current position is not mature. + // - The limit on closed positions has been reached. IEverlong.Position memory position; - while (!_portfolio.isEmpty()) { + uint256 count = 0; + while (!_portfolio.isEmpty() && count < _limit) { + // Retrieve the most mature position. position = _portfolio.head(); + + // If the position is not mature, return the output received thus + // far. if (!IHyperdrive(hyperdrive).isMature(position)) { return output; } + + // Close the position add the amount of assets received to the + // cumulative output. output += IHyperdrive(hyperdrive).closeLong( asBase, position, 0, "" ); + + // Update portfolio accounting to reflect the closed position. _portfolio.handleClosePosition(); + + // Increment the counter tracking the number of positions closed. + ++count; } } @@ -515,7 +600,7 @@ contract Everlong is IEverlong { /// bonds and the weighted average maturity of all positions. /// @return value The present portfolio value. function _calculateTotalAssets() internal view returns (uint256 value) { - value = ERC20(_asset).balanceOf(address(this)); + value = IERC20(_asset).balanceOf(address(this)); if (_portfolio.totalBonds != 0) { // NOTE: The maturity time is rounded to the next checkpoint to // underestimate the portfolio value. diff --git a/contracts/interfaces/IEverlong.sol b/contracts/interfaces/IEverlong.sol index 5c6dfa9..2e22189 100644 --- a/contracts/interfaces/IEverlong.sol +++ b/contracts/interfaces/IEverlong.sol @@ -24,6 +24,15 @@ abstract contract IEverlong is uint128 bondAmount; } + /// @notice Parameters to specify how a rebalance will be performed. + struct RebalanceOptions { + uint256 spendingOverride; + uint256 minOutput; + uint256 minVaultSharePrice; + uint256 positionClosureLimit; + bytes extraData; + } + // ╭─────────────────────────────────────────────────────────╮ // │ Getters │ // ╰─────────────────────────────────────────────────────────╯ @@ -58,4 +67,14 @@ abstract contract IEverlong is /// @notice Thrown when a redemption results in zero output assets. error RedemptionZeroOutput(); + + // ── Rebalancing ──────────────────────────────────────────── + + /// @notice Thrown when the spending override is below hyperdrive's + /// minimum transaction amount. + error SpendingOverrideTooLow(); + + /// @notice Thrown when the spending override is above the difference + /// between Everlong's balance and its max idle liquidity. + error SpendingOverrideTooHigh(); } diff --git a/contracts/interfaces/IEverlongPortfolio.sol b/contracts/interfaces/IEverlongPortfolio.sol index 18587df..58cbd58 100644 --- a/contracts/interfaces/IEverlongPortfolio.sol +++ b/contracts/interfaces/IEverlongPortfolio.sol @@ -8,9 +8,20 @@ interface IEverlongPortfolio { // │ Stateful │ // ╰─────────────────────────────────────────────────────────╯ + /// @notice Rebalances the Everlong bond portfolio if needed. + /// @param _options Options to control the rebalance behavior. + function rebalance(IEverlong.RebalanceOptions memory _options) external; + /// @notice Rebalances the Everlong bond portfolio if needed. function rebalance() external; + /// @notice Closes mature positions in the Everlong portfolio. + /// @param _limit The maximum number of positions to close. + /// @return output Amount of assets received from the closed positions. + function closeMaturedPositions( + uint256 _limit + ) external returns (uint256 output); + // ╭─────────────────────────────────────────────────────────╮ // │ Getters │ // ╰─────────────────────────────────────────────────────────╯ @@ -35,6 +46,11 @@ interface IEverlongPortfolio { /// @return True if the portfolio can be rebalanced, false otherwise. function canRebalance() external view returns (bool); + /// @notice Returns whether Everlong has sufficient idle liquidity to open + /// a new position. + /// @return True if a new position can be opened, false otherwise. + function canOpenPosition() external view returns (bool); + /// @notice Returns the target percentage of idle liquidity to maintain. /// @dev Expressed as a fraction of ONE. /// @return The target percentage of idle liquidity to maintain. diff --git a/contracts/libraries/HyperdriveExecution.sol b/contracts/libraries/HyperdriveExecution.sol index 657fe71..e26141c 100644 --- a/contracts/libraries/HyperdriveExecution.sol +++ b/contracts/libraries/HyperdriveExecution.sol @@ -37,20 +37,48 @@ library HyperdriveExecutionLibrary { /// @dev Opens a long with hyperdrive using amount. /// @param _asBase Whether to use hyperdrive's base asset. /// @param _amount Amount of assets to spend. + /// @param _extraData Extra data to pass to hyperdrive. /// @return maturityTime Maturity timestamp of the opened position. /// @return bondAmount Amount of bonds received. function openLong( IHyperdrive self, bool _asBase, uint256 _amount, - bytes memory // unused extra data + bytes memory _extraData ) internal returns (uint256 maturityTime, uint256 bondAmount) { - // TODO: Slippage (maturityTime, bondAmount) = self.openLong( _amount, 0, 0, - IHyperdrive.Options(address(this), _asBase, "") + IHyperdrive.Options(address(this), _asBase, _extraData) + ); + emit IEverlongEvents.PositionOpened( + maturityTime.toUint128(), + bondAmount.toUint128() + ); + } + + /// @dev Opens a long with hyperdrive using amount. + /// @param _asBase Whether to use hyperdrive's base asset. + /// @param _amount Amount of assets to spend. + /// @param _minOutput Minimum amount of bonds to receive. + /// @param _minVaultSharePrice Minimum hyperdrive vault share price. + /// @param _extraData Extra data to pass to hyperdrive. + /// @return maturityTime Maturity timestamp of the opened position. + /// @return bondAmount Amount of bonds received. + function openLong( + IHyperdrive self, + bool _asBase, + uint256 _amount, + uint256 _minOutput, + uint256 _minVaultSharePrice, + bytes memory _extraData + ) internal returns (uint256 maturityTime, uint256 bondAmount) { + (maturityTime, bondAmount) = self.openLong( + _amount, + _minOutput, + _minVaultSharePrice, + IHyperdrive.Options(address(this), _asBase, _extraData) ); emit IEverlongEvents.PositionOpened( maturityTime.toUint128(), diff --git a/test/harnesses/EverlongTest.sol b/test/harnesses/EverlongTest.sol index 1f42818..d912f25 100644 --- a/test/harnesses/EverlongTest.sol +++ b/test/harnesses/EverlongTest.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.20; import { console2 as console } from "forge-std/console2.sol"; import { HyperdriveTest } from "hyperdrive/test/utils/HyperdriveTest.sol"; import { ERC20Mintable } from "hyperdrive/contracts/test/ERC20Mintable.sol"; +import { HyperdriveUtils } from "hyperdrive/test/utils/HyperdriveUtils.sol"; import { IEverlongEvents } from "../../contracts/interfaces/IEverlongEvents.sol"; import { IEverlong } from "../../contracts/interfaces/IEverlong.sol"; import { EverlongExposed } from "../exposed/EverlongExposed.sol"; @@ -13,6 +14,7 @@ import { EverlongExposed } from "../exposed/EverlongExposed.sol"; /// @dev Everlong testing harness contract. /// @dev Tests should extend this contract and call its `setUp` function. contract EverlongTest is HyperdriveTest, IEverlongEvents { + using HyperdriveUtils for *; // ── Hyperdrive Storage ────────────────────────────────────────────── // address alice // address bob @@ -101,7 +103,7 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { vm.stopPrank(); // Fast forward and accrue some interest. - advanceTimeWithCheckpoints(POSITION_DURATION * 2, VARIABLE_RATE); + advanceTimeWithCheckpoints(POSITION_DURATION * 2); } // ╭─────────────────────────────────────────────────────────╮ @@ -111,6 +113,55 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { function depositEverlong( uint256 _amount, address _depositor + ) internal returns (uint256 shares) { + return + depositEverlong( + _amount, + _depositor, + true, + IEverlong.RebalanceOptions({ + spendingOverride: 0, + minOutput: 0, + minVaultSharePrice: 0, + positionClosureLimit: type(uint256).max, + extraData: "" + }) + ); + } + + function depositEverlong( + uint256 _amount, + address _depositor, + bool _shouldRebalance + ) internal returns (uint256 shares) { + return + depositEverlong( + _amount, + _depositor, + _shouldRebalance, + IEverlong.RebalanceOptions({ + spendingOverride: 0, + minOutput: 0, + minVaultSharePrice: 0, + positionClosureLimit: type(uint256).max, + extraData: "" + }) + ); + } + + function depositEverlong( + uint256 _amount, + address _depositor, + IEverlong.RebalanceOptions memory _rebalanceOptions + ) internal returns (uint256 shares) { + return depositEverlong(_amount, _depositor, true, _rebalanceOptions); + } + + function depositEverlong( + uint256 _amount, + address _depositor, + bool _shouldRebalance, + IEverlong.RebalanceOptions memory _rebalanceOptions ) internal returns (uint256 shares) { // Resolve the appropriate token. ERC20Mintable token = ERC20Mintable(everlong.asset()); @@ -130,18 +181,81 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { shares = everlong.deposit(_amount, _depositor); vm.stopPrank(); + // Rebalance if specified. + if (_shouldRebalance) { + rebalance(_rebalanceOptions); + } + // Return the amount of shares issued to _depositor for the deposit. return shares; } + // ╭─────────────────────────────────────────────────────────╮ + // │ Redeem Helpers │ + // ╰─────────────────────────────────────────────────────────╯ + function redeemEverlong( uint256 _amount, address _redeemer + ) internal returns (uint256 shares) { + return + redeemEverlong( + _amount, + _redeemer, + true, + IEverlong.RebalanceOptions({ + spendingOverride: 0, + minOutput: 0, + minVaultSharePrice: 0, + positionClosureLimit: type(uint256).max, + extraData: "" + }) + ); + } + + function redeemEverlong( + uint256 _amount, + address _redeemer, + bool _shouldRebalance + ) internal returns (uint256 shares) { + return + redeemEverlong( + _amount, + _redeemer, + _shouldRebalance, + IEverlong.RebalanceOptions({ + spendingOverride: 0, + minOutput: 0, + minVaultSharePrice: 0, + positionClosureLimit: type(uint256).max, + extraData: "" + }) + ); + } + + function redeemEverlong( + uint256 _amount, + address _redeemer, + IEverlong.RebalanceOptions memory _rebalanceOptions + ) internal returns (uint256 shares) { + return redeemEverlong(_amount, _redeemer, true, _rebalanceOptions); + } + + function redeemEverlong( + uint256 _amount, + address _redeemer, + bool _shouldRebalance, + IEverlong.RebalanceOptions memory _rebalanceOptions ) internal returns (uint256 proceeds) { // Make the redemption. vm.startPrank(_redeemer); proceeds = everlong.redeem(_amount, _redeemer, _redeemer); vm.stopPrank(); + + // Rebalance if specified. + if (_shouldRebalance) { + rebalance(_rebalanceOptions); + } } // TODO: This is gross, will refactor @@ -160,6 +274,92 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { vm.stopPrank(); } + // ╭─────────────────────────────────────────────────────────╮ + // │ Rebalancing │ + // ╰─────────────────────────────────────────────────────────╯ + + function rebalance() internal virtual { + vm.startPrank(everlong.admin()); + everlong.rebalance(); + vm.stopPrank(); + } + + function rebalance( + IEverlong.RebalanceOptions memory _options + ) internal virtual { + vm.startPrank(everlong.admin()); + everlong.rebalance(_options); + vm.stopPrank(); + } + + // ╭─────────────────────────────────────────────────────────╮ + // │ Advance Time Helpers │ + // ╰─────────────────────────────────────────────────────────╯ + + function advanceTimeWithCheckpoints(uint256 _time) internal virtual { + advanceTimeWithCheckpoints(_time, VARIABLE_RATE); + } + + function advanceTimeWithRebalancing( + uint256 _time, + uint256 _rebalanceInterval + ) internal virtual { + uint256 startTimeElapsed = block.timestamp; + // Note: if time % _rebalanceInterval != 0 then it ends up + // advancing time to the next _rebalanceInterval. + while (block.timestamp - startTimeElapsed < _time) { + advanceTime(_rebalanceInterval, VARIABLE_RATE); + rebalance(); + } + } + + function advanceTimeWithRebalancing( + uint256 _time, + uint256 _rebalanceInterval, + IEverlong.RebalanceOptions memory _options + ) internal virtual { + uint256 startTimeElapsed = block.timestamp; + // Note: if time % _rebalanceInterval != 0 then it ends up + // advancing time to the next _rebalanceInterval. + while (block.timestamp - startTimeElapsed < _time) { + advanceTime(_rebalanceInterval, VARIABLE_RATE); + rebalance(_options); + } + } + + function advanceTimeWithCheckpointsAndRebalancing( + uint256 _time + ) internal virtual { + uint256 startTimeElapsed = block.timestamp; + // Note: if time % CHECKPOINT_DURATION != 0 then it ends up + // advancing time to the next checkpoint. + while (block.timestamp - startTimeElapsed < _time) { + advanceTime(CHECKPOINT_DURATION, VARIABLE_RATE); + hyperdrive.checkpoint( + HyperdriveUtils.latestCheckpoint(hyperdrive), + 0 + ); + rebalance(); + } + } + + function advanceTimeWithCheckpointsAndRebalancing( + uint256 _time, + IEverlong.RebalanceOptions memory _options + ) internal virtual { + uint256 startTimeElapsed = block.timestamp; + // Note: if time % CHECKPOINT_DURATION != 0 then it ends up + // advancing time to the next checkpoint. + while (block.timestamp - startTimeElapsed < _time) { + advanceTime(CHECKPOINT_DURATION, VARIABLE_RATE); + hyperdrive.checkpoint( + HyperdriveUtils.latestCheckpoint(hyperdrive), + 0 + ); + rebalance(_options); + } + } + // ╭─────────────────────────────────────────────────────────╮ // │ Positions │ // ╰─────────────────────────────────────────────────────────╯ diff --git a/test/integration/CloseImmatureLongs.t.sol b/test/integration/CloseImmatureLongs.t.sol index 81427fb..a6385f5 100644 --- a/test/integration/CloseImmatureLongs.t.sol +++ b/test/integration/CloseImmatureLongs.t.sol @@ -144,10 +144,10 @@ contract CloseImmatureLongs is EverlongTest { uint256 basePaid = 10_000e18; ERC20Mintable(everlong.asset()).mint(basePaid); ERC20Mintable(everlong.asset()).approve(address(everlong), basePaid); - uint256 shares = everlong.deposit(basePaid, bob); + uint256 shares = depositEverlong(basePaid, bob); // half term passes - advanceTimeWithCheckpoints(POSITION_DURATION / 2, variableInterest); + advanceTimeWithCheckpointsAndRebalancing(POSITION_DURATION / 2); // Estimate the proceeds. uint256 estimatedProceeds = everlong.previewRedeem(shares); @@ -155,7 +155,7 @@ contract CloseImmatureLongs is EverlongTest { console.log("totalAssets: %e", everlong.totalAssets()); // Close the long. - uint256 baseProceeds = everlong.redeem(shares, bob, bob); + uint256 baseProceeds = redeemEverlong(shares, bob); console.log("actual: %s", baseProceeds); console.log( "assets: %s", diff --git a/test/integration/PartialClosures.t.sol b/test/integration/PartialClosures.t.sol index ac18196..77af9fa 100644 --- a/test/integration/PartialClosures.t.sol +++ b/test/integration/PartialClosures.t.sol @@ -50,7 +50,7 @@ contract PartialClosures is EverlongTest { 0.8e18 ); uint256 aliceRedeemAmount = aliceShares.mulDown(_redemptionPercentage); - redeemEverlong(aliceRedeemAmount, alice); + redeemEverlong(aliceRedeemAmount, alice, false); uint256 positionBondsAfterRedeem = everlong.totalBonds(); // Ensure Everlong still has a position open. @@ -92,9 +92,8 @@ contract PartialClosures is EverlongTest { uint256 aliceShares = depositEverlong(aliceDepositAmount, alice); // Time advances towards the end of the term. - advanceTimeWithCheckpoints( - POSITION_DURATION.mulDown(0.9e18), - VARIABLE_RATE + advanceTimeWithCheckpointsAndRebalancing( + POSITION_DURATION.mulDown(0.8e18) ); // Alice deposits again into Everlong. diff --git a/test/integration/VaultSharePriceManipulation.t.sol b/test/integration/VaultSharePriceManipulation.t.sol index 087f65a..d271619 100644 --- a/test/integration/VaultSharePriceManipulation.t.sol +++ b/test/integration/VaultSharePriceManipulation.t.sol @@ -408,7 +408,7 @@ contract VaultSharePriceManipulation is EverlongTest { ); if (params.timeToCloseShort > 0) { - advanceTimeWithCheckpoints(params.timeToCloseShort, VARIABLE_RATE); + advanceTimeWithCheckpointsAndRebalancing(params.timeToCloseShort); if (everlong.canRebalance()) { everlong.rebalance(); } @@ -426,9 +426,8 @@ contract VaultSharePriceManipulation is EverlongTest { } if (params.timeToCloseEverlong > 0) { - advanceTimeWithCheckpoints( - params.timeToCloseEverlong, - VARIABLE_RATE + advanceTimeWithCheckpointsAndRebalancing( + params.timeToCloseEverlong ); if (everlong.canRebalance()) { everlong.rebalance(); @@ -443,9 +442,8 @@ contract VaultSharePriceManipulation is EverlongTest { } if (params.bystanderCloseDelay > 0) { - advanceTimeWithCheckpoints( - params.bystanderCloseDelay, - VARIABLE_RATE + advanceTimeWithCheckpointsAndRebalancing( + params.bystanderCloseDelay ); if (everlong.canRebalance()) { everlong.rebalance(); diff --git a/test/units/EverlongERC4626.t.sol b/test/units/EverlongERC4626.t.sol index 14eb27b..6b099c7 100644 --- a/test/units/EverlongERC4626.t.sol +++ b/test/units/EverlongERC4626.t.sol @@ -67,7 +67,7 @@ contract TestEverlongERC4626 is EverlongTest { uint256 shares = depositEverlong(amount, alice); // Fast forward to halfway through maturity. - advanceTime(POSITION_DURATION / 2, VARIABLE_RATE); + advanceTimeWithCheckpointsAndRebalancing(POSITION_DURATION / 2); // Ensure that previewRedeem output is at most equal to actual output // and within margins. @@ -86,7 +86,7 @@ contract TestEverlongERC4626 is EverlongTest { uint256 shares = depositEverlong(amount, alice); // Fast forward to halfway through maturity. - advanceTime(POSITION_DURATION / 2, VARIABLE_RATE); + advanceTimeWithCheckpointsAndRebalancing(POSITION_DURATION / 2); // Ensure that previewRedeem output is at most equal to actual output // and within margins. diff --git a/test/units/EverlongPortfolio.t.sol b/test/units/EverlongPortfolio.t.sol index 9315f5e..3318e74 100644 --- a/test/units/EverlongPortfolio.t.sol +++ b/test/units/EverlongPortfolio.t.sol @@ -10,7 +10,7 @@ import { Portfolio } from "../../contracts/libraries/Portfolio.sol"; import { EverlongTest } from "../harnesses/EverlongTest.sol"; /// @dev Tests for Everlong position management functionality. -contract TestEverlongPositions is EverlongTest { +contract TestEverlongPortfolio is EverlongTest { using Portfolio for Portfolio.State; Portfolio.State public portfolio; @@ -31,8 +31,6 @@ contract TestEverlongPositions is EverlongTest { } function setUp() public virtual override { - TARGET_IDLE_LIQUIDITY_PERCENTAGE = 0.1e18; - MAX_IDLE_LIQUIDITY_PERCENTAGE = 0.2e18; super.setUp(); deployEverlong(); } @@ -168,7 +166,7 @@ contract TestEverlongPositions is EverlongTest { ); // Mature the first position (second will be unmature). - advanceTime(POSITION_DURATION, 0); + advanceTimeWithCheckpoints(POSITION_DURATION); // Check that `hasMaturedPositions()` returns true. assertTrue( @@ -222,12 +220,12 @@ contract TestEverlongPositions is EverlongTest { function test_canRebalance_with_matured_position() external { // Mint some tokens to Everlong for opening longs and rebalance. mintApproveEverlongBaseAsset(address(everlong), 100e18); - everlong.rebalance(); + rebalance(); // Increase block.timestamp until position is mature. // Ensure Everlong has a matured position. // Ensure `canRebalance()` returns true. - advanceTime(everlong.positionAt(0).maturityTime, 0); + advanceTimeWithCheckpoints(everlong.positionAt(0).maturityTime); assertTrue( everlong.hasMaturedPositions(), "everlong should have matured position after advancing time" @@ -247,9 +245,11 @@ contract TestEverlongPositions is EverlongTest { function test_rebalance_state() external { // Mint some tokens to Everlong for opening longs and rebalance. mintApproveEverlongBaseAsset(address(everlong), 10_000e18); - everlong.rebalance(); - advanceTime(everlong.positionAt(0).maturityTime, 0); - everlong.rebalance(); + rebalance(); + advanceTimeWithCheckpointsAndRebalancing( + everlong.positionAt(0).maturityTime + ); + rebalance(); // Ensure idle liquidity is close to target. assertApproxEqAbs( @@ -267,4 +267,137 @@ contract TestEverlongPositions is EverlongTest { // Ensure no matured positions assertFalse(everlong.hasMaturedPositions()); } + + /// @dev Tests the functionality of `RebalanceOptions.positionClosureLimit`. + function test_rebalance_options_positionClosureLimit() external { + // Create three positions in Everlong. + mintApproveEverlongBaseAsset(address(everlong), 10_000e18); + rebalance(); + advanceTimeWithCheckpoints(POSITION_DURATION / 10); + mintApproveEverlongBaseAsset(address(everlong), 10_000e18); + rebalance(); + advanceTimeWithCheckpoints(POSITION_DURATION / 10); + mintApproveEverlongBaseAsset(address(everlong), 10_000e18); + rebalance(); + advanceTimeWithCheckpoints(POSITION_DURATION / 10); + + // Fast forward time so they are all mature. + advanceTimeWithCheckpoints(POSITION_DURATION * 2); + + // Ensure that Everlong has 3 mature positions. + assertEq(everlong.positionCount(), 3); + assertTrue(everlong.positionAt(0).maturityTime < block.timestamp); + assertTrue(everlong.positionAt(1).maturityTime < block.timestamp); + assertTrue(everlong.positionAt(2).maturityTime < block.timestamp); + + // Call rebalance with `positionClosureLimit` set to zero. + rebalance( + IEverlong.RebalanceOptions({ + spendingOverride: 0, + minOutput: 0, + minVaultSharePrice: 0, + positionClosureLimit: 0, + extraData: "" + }) + ); + + // Ensure that Everlong still has 3 matured positions. + assertEq(everlong.positionCount(), 3); + assertTrue(everlong.positionAt(0).maturityTime < block.timestamp); + assertTrue(everlong.positionAt(1).maturityTime < block.timestamp); + assertTrue(everlong.positionAt(2).maturityTime < block.timestamp); + } + + /// @dev Tests the functionality of `RebalanceOptions.spendingOverride`. + function test_rebalance_options_spendingOverride() external { + // Mint Everlong some assets. + mintApproveEverlongBaseAsset(address(everlong), 10_000e18); + + // Try rebalancing with too low of a `spendingOverride`. + vm.startPrank(everlong.admin()); + vm.expectRevert(IEverlong.SpendingOverrideTooLow.selector); + everlong.rebalance( + IEverlong.RebalanceOptions({ + spendingOverride: 1, + minOutput: 0, + minVaultSharePrice: 0, + positionClosureLimit: type(uint256).max, + extraData: "" + }) + ); + vm.stopPrank(); + + // Try rebalancing with too high of a `spendingOverride`. + uint256 balance = IERC20(everlong.asset()).balanceOf(address(everlong)); + uint256 targetIdle = everlong.targetIdleLiquidity(); + vm.startPrank(everlong.admin()); + vm.expectRevert(IEverlong.SpendingOverrideTooHigh.selector); + everlong.rebalance( + IEverlong.RebalanceOptions({ + spendingOverride: (balance - targetIdle) * 2, + minOutput: 0, + minVaultSharePrice: 0, + positionClosureLimit: type(uint256).max, + extraData: "" + }) + ); + vm.stopPrank(); + + // Try rebalancing with an acceptable `spendingOverride`. + uint256 maxIdle = everlong.maxIdleLiquidity(); + vm.startPrank(everlong.admin()); + everlong.rebalance( + IEverlong.RebalanceOptions({ + spendingOverride: (balance - maxIdle) / 2, + minOutput: 0, + minVaultSharePrice: 0, + positionClosureLimit: type(uint256).max, + extraData: "" + }) + ); + vm.stopPrank(); + + // Ensure that after rebalancing, the idle liquidity is still greater + // than the maximum. + assertGt( + IERC20(everlong.asset()).balanceOf(address(everlong)), + everlong.maxIdleLiquidity() + ); + } + + /// @dev Tests the functionality of `RebalanceOptions.minOutput`. + function test_rebalance_options_minOutput() external { + // Mint Everlong some assets. + mintApproveEverlongBaseAsset(address(everlong), 10_000e18); + + // Rebalancing with an incredibly high `minOutput` should fail. + vm.expectRevert(); + everlong.rebalance( + IEverlong.RebalanceOptions({ + spendingOverride: 0, + minOutput: type(uint256).max, + minVaultSharePrice: 0, + positionClosureLimit: type(uint256).max, + extraData: "" + }) + ); + } + + /// @dev Tests the functionality of `RebalanceOptions.minVaultSharePrice`. + function test_rebalance_options_minVaultSharePrice() external { + // Mint Everlong some assets. + mintApproveEverlongBaseAsset(address(everlong), 10_000e18); + + // Rebalancing with an incredibly high `minVaultSharePrice` should fail. + vm.expectRevert(); + everlong.rebalance( + IEverlong.RebalanceOptions({ + spendingOverride: 0, + minOutput: 0, + minVaultSharePrice: type(uint256).max, + positionClosureLimit: type(uint256).max, + extraData: "" + }) + ); + } } diff --git a/test/units/HyperdriveExecution.t.sol b/test/units/HyperdriveExecution.t.sol index 8db1ca8..6d60ec8 100644 --- a/test/units/HyperdriveExecution.t.sol +++ b/test/units/HyperdriveExecution.t.sol @@ -107,7 +107,7 @@ contract TestHyperdriveExecution is EverlongTest { (uint256 maturityTime, uint256 bondAmount) = openLong(alice, amount); // Advance halfway through the term. - advanceTimeWithCheckpoints(POSITION_DURATION / 2, VARIABLE_RATE); + advanceTimeWithCheckpointsAndRebalancing(POSITION_DURATION / 2); // Ensure the preview amount underestimates the actual and is // within the tolerance. @@ -135,7 +135,7 @@ contract TestHyperdriveExecution is EverlongTest { (uint256 maturityTime, uint256 bondAmount) = openLong(alice, amount); // Advance halfway through the term. - advanceTimeWithCheckpoints(POSITION_DURATION / 2, VARIABLE_RATE); + advanceTimeWithCheckpointsAndRebalancing(POSITION_DURATION / 2); // Ensure the preview amount underestimates the actual and is // within the tolerance. @@ -160,7 +160,7 @@ contract TestHyperdriveExecution is EverlongTest { (uint256 maturityTime, uint256 bondAmount) = openLong(alice, amount); // Advance halfway through the term. - advanceTimeWithCheckpoints(POSITION_DURATION, VARIABLE_RATE); + advanceTimeWithCheckpointsAndRebalancing(POSITION_DURATION); // Ensure the preview amount underestimates the actual and is // within the tolerance. From 5c46a07b55524cb5283d2e50eb50ac7e920819a6 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Wed, 30 Oct 2024 12:16:36 -0500 Subject: [PATCH 12/16] notes for future self --- contracts/interfaces/IEverlong.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/interfaces/IEverlong.sol b/contracts/interfaces/IEverlong.sol index 2e22189..3e99d1e 100644 --- a/contracts/interfaces/IEverlong.sol +++ b/contracts/interfaces/IEverlong.sol @@ -24,6 +24,8 @@ abstract contract IEverlong is uint128 bondAmount; } + // TODO: Revisit position closure limit to see what position duration would + // be needed to run out of gas. /// @notice Parameters to specify how a rebalance will be performed. struct RebalanceOptions { uint256 spendingOverride; From 848117c50c05563f7e55cd626e3bbb20bb0d2aa3 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Thu, 31 Oct 2024 02:06:11 -0500 Subject: [PATCH 13/16] better commenting and devex --- contracts/Everlong.sol | 8 +++++++- contracts/interfaces/IEverlong.sol | 10 +++++++++- test/harnesses/EverlongTest.sol | 8 ++++---- test/units/EverlongPortfolio.t.sol | 27 ++++++++++++++++++--------- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index bdd051e..f5ea974 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -318,7 +318,7 @@ contract Everlong is IEverlong { spendingOverride: 0, minOutput: 0, minVaultSharePrice: 0, - positionClosureLimit: type(uint256).max, + positionClosureLimit: 0, extraData: "" }) ); @@ -469,10 +469,16 @@ contract Everlong is IEverlong { /// @dev Close only matured positions in the portfolio. /// @param _limit The maximum number of positions to close. + /// A value of zero indicates no limit. /// @return output Proceeds of closing the matured positions. function _closeMaturedPositions( uint256 _limit ) internal returns (uint256 output) { + // A value of zero for `_limit` indicates no limit. + if (_limit == 0) { + _limit = type(uint256).max; + } + // Iterate through positions from most to least mature. // Exit if: // - There are no more positions. diff --git a/contracts/interfaces/IEverlong.sol b/contracts/interfaces/IEverlong.sol index 3e99d1e..f3041fc 100644 --- a/contracts/interfaces/IEverlong.sol +++ b/contracts/interfaces/IEverlong.sol @@ -24,14 +24,22 @@ abstract contract IEverlong is uint128 bondAmount; } - // TODO: Revisit position closure limit to see what position duration would + // TODO: Revisit position closure limit to see what POSITION_DURATION would // be needed to run out of gas. + // /// @notice Parameters to specify how a rebalance will be performed. struct RebalanceOptions { + /// @notice Limit on the amount of idle to spend on a new position. + /// @dev A value of zero indicates no limit. uint256 spendingOverride; + /// @notice Minimum amount of bonds to receive when opening a position. uint256 minOutput; + /// @notice Minimum vault share price when opening a position. uint256 minVaultSharePrice; + /// @notice Maximum amount of mature positions that can be closed. + /// @dev A value of zero indicates no limit. uint256 positionClosureLimit; + /// @notice Passed to hyperdrive `openLong()` and `closeLong()`. bytes extraData; } diff --git a/test/harnesses/EverlongTest.sol b/test/harnesses/EverlongTest.sol index d912f25..af2a882 100644 --- a/test/harnesses/EverlongTest.sol +++ b/test/harnesses/EverlongTest.sol @@ -123,7 +123,7 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { spendingOverride: 0, minOutput: 0, minVaultSharePrice: 0, - positionClosureLimit: type(uint256).max, + positionClosureLimit: 0, extraData: "" }) ); @@ -143,7 +143,7 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { spendingOverride: 0, minOutput: 0, minVaultSharePrice: 0, - positionClosureLimit: type(uint256).max, + positionClosureLimit: 0, extraData: "" }) ); @@ -207,7 +207,7 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { spendingOverride: 0, minOutput: 0, minVaultSharePrice: 0, - positionClosureLimit: type(uint256).max, + positionClosureLimit: 0, extraData: "" }) ); @@ -227,7 +227,7 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { spendingOverride: 0, minOutput: 0, minVaultSharePrice: 0, - positionClosureLimit: type(uint256).max, + positionClosureLimit: 0, extraData: "" }) ); diff --git a/test/units/EverlongPortfolio.t.sol b/test/units/EverlongPortfolio.t.sol index 3318e74..1c44711 100644 --- a/test/units/EverlongPortfolio.t.sol +++ b/test/units/EverlongPortfolio.t.sol @@ -236,6 +236,15 @@ contract TestEverlongPortfolio is EverlongTest { ); } + /// @dev Ensures that rebalance reverts when called by a non-admin + function test_rebalance_failure_unauthorized() external { + // Attempt calling rebalance as Dan (not the admin). + vm.startPrank(dan); + vm.expectRevert(IEverlong.Unauthorized.selector); + everlong.rebalance(); + vm.stopPrank(); + } + // TODO: Reduce tolerance on remaining idle liquidity. // /// @dev Ensures the following after a rebalance: @@ -290,22 +299,22 @@ contract TestEverlongPortfolio is EverlongTest { assertTrue(everlong.positionAt(1).maturityTime < block.timestamp); assertTrue(everlong.positionAt(2).maturityTime < block.timestamp); - // Call rebalance with `positionClosureLimit` set to zero. + // Call rebalance with `positionClosureLimit` set to one. rebalance( IEverlong.RebalanceOptions({ spendingOverride: 0, minOutput: 0, minVaultSharePrice: 0, - positionClosureLimit: 0, + positionClosureLimit: 1, extraData: "" }) ); - // Ensure that Everlong still has 3 matured positions. + // Ensure that Everlong still has 3 positions, the first 2 mature. assertEq(everlong.positionCount(), 3); assertTrue(everlong.positionAt(0).maturityTime < block.timestamp); assertTrue(everlong.positionAt(1).maturityTime < block.timestamp); - assertTrue(everlong.positionAt(2).maturityTime < block.timestamp); + assertTrue(everlong.positionAt(2).maturityTime > block.timestamp); } /// @dev Tests the functionality of `RebalanceOptions.spendingOverride`. @@ -321,7 +330,7 @@ contract TestEverlongPortfolio is EverlongTest { spendingOverride: 1, minOutput: 0, minVaultSharePrice: 0, - positionClosureLimit: type(uint256).max, + positionClosureLimit: 0, extraData: "" }) ); @@ -337,7 +346,7 @@ contract TestEverlongPortfolio is EverlongTest { spendingOverride: (balance - targetIdle) * 2, minOutput: 0, minVaultSharePrice: 0, - positionClosureLimit: type(uint256).max, + positionClosureLimit: 0, extraData: "" }) ); @@ -351,7 +360,7 @@ contract TestEverlongPortfolio is EverlongTest { spendingOverride: (balance - maxIdle) / 2, minOutput: 0, minVaultSharePrice: 0, - positionClosureLimit: type(uint256).max, + positionClosureLimit: 0, extraData: "" }) ); @@ -377,7 +386,7 @@ contract TestEverlongPortfolio is EverlongTest { spendingOverride: 0, minOutput: type(uint256).max, minVaultSharePrice: 0, - positionClosureLimit: type(uint256).max, + positionClosureLimit: 0, extraData: "" }) ); @@ -395,7 +404,7 @@ contract TestEverlongPortfolio is EverlongTest { spendingOverride: 0, minOutput: 0, minVaultSharePrice: type(uint256).max, - positionClosureLimit: type(uint256).max, + positionClosureLimit: 0, extraData: "" }) ); From ad4d2d7ac5d1fe7cfbb9e1502442f788750a6648 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Thu, 31 Oct 2024 16:10:11 -0500 Subject: [PATCH 14/16] testing cleanup + make rebalance opt-in for depositEverlong and redeemEverlong --- contracts/Everlong.sol | 52 +----- contracts/interfaces/IEverlongPortfolio.sol | 3 - test/exposed/EverlongERC4626Exposed.sol | 16 -- test/exposed/EverlongPortfolioExposed.sol | 32 ---- test/harnesses/EverlongTest.sol | 170 ++++++++++++------ test/integration/CloseImmatureLongs.t.sol | 8 +- test/integration/PartialClosures.t.sol | 10 +- test/integration/Sandwich.t.sol | 22 ++- .../VaultSharePriceManipulation.t.sol | 18 +- test/units/EverlongERC4626.t.sol | 8 +- test/units/EverlongPortfolio.t.sol | 4 +- 11 files changed, 158 insertions(+), 185 deletions(-) delete mode 100644 test/exposed/EverlongERC4626Exposed.sol delete mode 100644 test/exposed/EverlongPortfolioExposed.sol diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index f5ea974..8eac0cd 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -292,7 +292,7 @@ contract Everlong is IEverlong { // If we do not have enough balance to service the withdrawal after // closing any matured positions, close more positions. uint256 balance = IERC20(_asset).balanceOf(address(this)) + - _closeMaturedPositions(type(uint256).max); + closeMaturedPositions(type(uint256).max); if (_assets > balance) { _closePositions(_assets - balance); } @@ -306,52 +306,22 @@ contract Everlong is IEverlong { // │ Rebalancing │ // ╰─────────────────────────────────────────────────────────╯ - // NOTE: Errors from hyperdrive are not handled. The keeper must configure - // the correct parameters to avoid issues with insufficient liquidity - // and running out of gas from mature position closures. - // - /// @notice Rebalance the everlong portfolio by closing mature positions - /// and using the proceeds over target idle to open new positions. - function rebalance() external onlyAdmin { - _rebalance( - IEverlong.RebalanceOptions({ - spendingOverride: 0, - minOutput: 0, - minVaultSharePrice: 0, - positionClosureLimit: 0, - extraData: "" - }) - ); - } - - // NOTE: Errors from hyperdrive are not handled. The keeper must configure - // the correct parameters to avoid issues with insufficient liquidity - // and running out of gas from mature position closures. - // /// @notice Rebalance the everlong portfolio by closing mature positions /// and using the proceeds over target idle to open new positions. + /// @dev Errors from hyperdrive are not handled. The keeper must configure + /// the correct parameters to avoid issues with insufficient liquidity + /// and running out of gas from mature position closures. /// @param _options Options to control the rebalance behavior. function rebalance( IEverlong.RebalanceOptions memory _options ) external onlyAdmin { - _rebalance(_options); - } - - // NOTE: Errors from hyperdrive are not handled. The keeper must configure - // the correct parameters to avoid issues with insufficient liquidity - // and running out of gas from mature position closures. - // - /// @dev Rebalance the everlong portfolio by closing mature positions - /// and using the proceeds over target idle to open new positions. - /// @param _options Options to control the rebalance behavior. - function _rebalance(IEverlong.RebalanceOptions memory _options) internal { // Early return if no rebalancing is needed. if (!canRebalance()) { return; } // Close matured positions. - _closeMaturedPositions(_options.positionClosureLimit); + closeMaturedPositions(_options.positionClosureLimit); // If Everlong has sufficient idle, open a new position. if (canOpenPosition()) { @@ -467,13 +437,13 @@ contract Everlong is IEverlong { // │ Hyperdrive │ // ╰─────────────────────────────────────────────────────────╯ - /// @dev Close only matured positions in the portfolio. + /// @notice Close only matured positions in the portfolio. /// @param _limit The maximum number of positions to close. /// A value of zero indicates no limit. /// @return output Proceeds of closing the matured positions. - function _closeMaturedPositions( + function closeMaturedPositions( uint256 _limit - ) internal returns (uint256 output) { + ) public returns (uint256 output) { // A value of zero for `_limit` indicates no limit. if (_limit == 0) { _limit = type(uint256).max; @@ -485,8 +455,7 @@ contract Everlong is IEverlong { // - The current position is not mature. // - The limit on closed positions has been reached. IEverlong.Position memory position; - uint256 count = 0; - while (!_portfolio.isEmpty() && count < _limit) { + for (uint256 count; !_portfolio.isEmpty() && count < _limit; ++count) { // Retrieve the most mature position. position = _portfolio.head(); @@ -507,9 +476,6 @@ contract Everlong is IEverlong { // Update portfolio accounting to reflect the closed position. _portfolio.handleClosePosition(); - - // Increment the counter tracking the number of positions closed. - ++count; } } diff --git a/contracts/interfaces/IEverlongPortfolio.sol b/contracts/interfaces/IEverlongPortfolio.sol index 58cbd58..1895de8 100644 --- a/contracts/interfaces/IEverlongPortfolio.sol +++ b/contracts/interfaces/IEverlongPortfolio.sol @@ -12,9 +12,6 @@ interface IEverlongPortfolio { /// @param _options Options to control the rebalance behavior. function rebalance(IEverlong.RebalanceOptions memory _options) external; - /// @notice Rebalances the Everlong bond portfolio if needed. - function rebalance() external; - /// @notice Closes mature positions in the Everlong portfolio. /// @param _limit The maximum number of positions to close. /// @return output Amount of assets received from the closed positions. diff --git a/test/exposed/EverlongERC4626Exposed.sol b/test/exposed/EverlongERC4626Exposed.sol deleted file mode 100644 index e93c5fd..0000000 --- a/test/exposed/EverlongERC4626Exposed.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.20; - -import { Everlong } from "../../contracts/Everlong.sol"; - -/// @title EverlongERC4626Exposed -/// @dev Exposes all internal functions for the `EverlongERC4626` contract. -abstract contract EverlongERC4626Exposed is Everlong { - function exposed_beforeWithdraw(uint256 _assets, uint256 _shares) public { - return _beforeWithdraw(_assets, _shares); - } - - function exposed_afterDeposit(uint256 _assets, uint256 _shares) public { - return _afterDeposit(_assets, _shares); - } -} diff --git a/test/exposed/EverlongPortfolioExposed.sol b/test/exposed/EverlongPortfolioExposed.sol deleted file mode 100644 index 231d963..0000000 --- a/test/exposed/EverlongPortfolioExposed.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.20; - -import { IEverlong } from "../../contracts/interfaces/IEverlong.sol"; -import { Everlong } from "../../contracts/Everlong.sol"; -import { Portfolio } from "../../contracts/libraries/Portfolio.sol"; - -/// @title EverlongPortfolioExposed -/// @dev Exposes all internal functions for the `EverlongPositions` contract. -abstract contract EverlongPortfolioExposed is Everlong { - using Portfolio for Portfolio.State; - - function exposed_handleOpenPosition( - IEverlong.Position memory _position - ) public { - _portfolio.handleOpenPosition( - _position.maturityTime, - _position.bondAmount - ); - } - - function exposed_handleOpenPosition( - uint256 _maturityTime, - uint256 _bondAmount - ) public { - _portfolio.handleOpenPosition(_maturityTime, _bondAmount); - } - - function exposed_handleClosePosition() public { - _portfolio.handleClosePosition(); - } -} diff --git a/test/harnesses/EverlongTest.sol b/test/harnesses/EverlongTest.sol index af2a882..723258e 100644 --- a/test/harnesses/EverlongTest.sol +++ b/test/harnesses/EverlongTest.sol @@ -49,6 +49,15 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { uint256 internal TARGET_IDLE_LIQUIDITY_PERCENTAGE = 0.1e18; uint256 internal MAX_IDLE_LIQUIDITY_PERCENTAGE = 0.2e18; + IEverlong.RebalanceOptions internal DEFAULT_REBALANCE_OPTIONS = + IEverlong.RebalanceOptions({ + spendingOverride: 0, + minOutput: 0, + minVaultSharePrice: 0, + positionClosureLimit: 0, + extraData: "" + }); + // ╭─────────────────────────────────────────────────────────╮ // │ Hyperdrive Configuration │ // ╰─────────────────────────────────────────────────────────╯ @@ -110,6 +119,10 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { // │ Deposit Helpers │ // ╰─────────────────────────────────────────────────────────╯ + /// @dev Deposit into Everlong. + /// @param _amount Amount of assets to deposit. + /// @param _depositor Address to deposit as. + /// @return shares Amount of shares received from the deposit. function depositEverlong( uint256 _amount, address _depositor @@ -118,7 +131,7 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { depositEverlong( _amount, _depositor, - true, + false, IEverlong.RebalanceOptions({ spendingOverride: 0, minOutput: 0, @@ -129,6 +142,11 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { ); } + /// @dev Deposit into Everlong. + /// @param _amount Amount of assets to deposit. + /// @param _depositor Address to deposit as. + /// @param _shouldRebalance Whether to rebalance after the deposit is made. + /// @return shares Amount of shares received from the deposit. function depositEverlong( uint256 _amount, address _depositor, @@ -149,6 +167,11 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { ); } + /// @dev Deposit into Everlong and rebalance after the deposit. + /// @param _amount Amount of assets to deposit. + /// @param _depositor Address to deposit as. + /// @param _rebalanceOptions Options to pass to the rebalance call. + /// @return shares Amount of shares received from the deposit. function depositEverlong( uint256 _amount, address _depositor, @@ -157,12 +180,14 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { return depositEverlong(_amount, _depositor, true, _rebalanceOptions); } + // NOTE: Core functionality for all `depositEverlong(..)` overloads. + // This is the most verbose, probably don't want to call it directly. function depositEverlong( uint256 _amount, address _depositor, bool _shouldRebalance, IEverlong.RebalanceOptions memory _rebalanceOptions - ) internal returns (uint256 shares) { + ) private returns (uint256 shares) { // Resolve the appropriate token. ERC20Mintable token = ERC20Mintable(everlong.asset()); @@ -194,53 +219,70 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { // │ Redeem Helpers │ // ╰─────────────────────────────────────────────────────────╯ + /// @dev Redeem shares from Everlong. + /// @param _shares Amount of shares to redeem. + /// @param _redeemer Address to redeem as. + /// @return assets Amount of assets received from the redemption. function redeemEverlong( - uint256 _amount, + uint256 _shares, address _redeemer - ) internal returns (uint256 shares) { - return - redeemEverlong( - _amount, - _redeemer, - true, - IEverlong.RebalanceOptions({ - spendingOverride: 0, - minOutput: 0, - minVaultSharePrice: 0, - positionClosureLimit: 0, - extraData: "" - }) - ); + ) internal returns (uint256 assets) { + assets = redeemEverlong( + _shares, + _redeemer, + true, + IEverlong.RebalanceOptions({ + spendingOverride: 0, + minOutput: 0, + minVaultSharePrice: 0, + positionClosureLimit: 0, + extraData: "" + }) + ); + return assets; } + /// @dev Redeem shares from Everlong. + /// @param _shares Amount of shares to redeem. + /// @param _redeemer Address to redeem as. + /// @param _shouldRebalance Whether to rebalance after the redeem is made. + /// @return assets Amount of assets received from the redemption. function redeemEverlong( - uint256 _amount, + uint256 _shares, address _redeemer, bool _shouldRebalance - ) internal returns (uint256 shares) { - return - redeemEverlong( - _amount, - _redeemer, - _shouldRebalance, - IEverlong.RebalanceOptions({ - spendingOverride: 0, - minOutput: 0, - minVaultSharePrice: 0, - positionClosureLimit: 0, - extraData: "" - }) - ); + ) internal returns (uint256 assets) { + assets = redeemEverlong( + _shares, + _redeemer, + _shouldRebalance, + IEverlong.RebalanceOptions({ + spendingOverride: 0, + minOutput: 0, + minVaultSharePrice: 0, + positionClosureLimit: 0, + extraData: "" + }) + ); + return assets; } + /// @dev Redeem shares from Everlong. + /// @param _shares Amount of shares to redeem. + /// @param _redeemer Address to redeem as. + /// @param _rebalanceOptions Options to pass to the rebalance call. + /// @return assets Amount of assets received from the redemption. function redeemEverlong( - uint256 _amount, + uint256 _shares, address _redeemer, IEverlong.RebalanceOptions memory _rebalanceOptions - ) internal returns (uint256 shares) { - return redeemEverlong(_amount, _redeemer, true, _rebalanceOptions); + ) internal returns (uint256 assets) { + assets = redeemEverlong(_shares, _redeemer, true, _rebalanceOptions); + return assets; } + // NOTE: Core functionality for all `redeemEverlong(..)` overloads. + // This is the most verbose, probably don't want to call it directly. function redeemEverlong( uint256 _amount, address _redeemer, @@ -258,18 +300,18 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { } } - // TODO: This is gross, will refactor - /// @dev Mint base token to the provided address a - /// and approve the Everlong contract. + /// @dev Mint base token to the provided address and approve Everlong. + /// @param _recipient Receiver of the minted assets. + /// @param _amount Amount of assets to mint. function mintApproveEverlongBaseAsset( - address recipient, - uint256 amount + address _recipient, + uint256 _amount ) internal { - ERC20Mintable(hyperdrive.baseToken()).mint(recipient, amount); - vm.startPrank(recipient); + ERC20Mintable(hyperdrive.baseToken()).mint(_recipient, _amount); + vm.startPrank(_recipient); ERC20Mintable(hyperdrive.baseToken()).approve( address(everlong), - amount + _amount ); vm.stopPrank(); } @@ -278,12 +320,15 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { // │ Rebalancing │ // ╰─────────────────────────────────────────────────────────╯ + /// @dev Call `everlong.rebalance(...)` as the admin with default options. function rebalance() internal virtual { vm.startPrank(everlong.admin()); - everlong.rebalance(); + everlong.rebalance(DEFAULT_REBALANCE_OPTIONS); vm.stopPrank(); } + /// @dev Call `everlong.rebalance(...)` as the admin with provided options. + /// @param _options Rebalance options to pass to Everlong. function rebalance( IEverlong.RebalanceOptions memory _options ) internal virtual { @@ -296,10 +341,17 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { // │ Advance Time Helpers │ // ╰─────────────────────────────────────────────────────────╯ + /// @dev Advance time by the specified amount at the global variable rate. + /// @param _time Amount of time to advance. function advanceTimeWithCheckpoints(uint256 _time) internal virtual { advanceTimeWithCheckpoints(_time, VARIABLE_RATE); } + /// @dev Advance time and rebalance on the specified interval. + /// @dev If time % _rebalanceInterval != 0 then it ends up advancing time + /// to the next _rebalanceInterval. + /// @param _time Amount of time to advance. + /// @param _rebalanceInterval Amount of time between rebalances. function advanceTimeWithRebalancing( uint256 _time, uint256 _rebalanceInterval @@ -313,6 +365,12 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { } } + /// @dev Advance time and rebalance on the specified interval. + /// @dev If time % _rebalanceInterval != 0 then it ends up advancing time + /// to the next _rebalanceInterval. + /// @param _time Amount of time to advance. + /// @param _rebalanceInterval Amount of time between rebalances. + /// @param _options Rebalance options to pass to Everlong. function advanceTimeWithRebalancing( uint256 _time, uint256 _rebalanceInterval, @@ -327,6 +385,10 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { } } + /// @dev Advance time, create checkpoints, and rebalance at checkpoints. + /// @dev If time % _rebalanceInterval != 0 then it ends up advancing time + /// to the next _rebalanceInterval. + /// @param _time Amount of time to advance. function advanceTimeWithCheckpointsAndRebalancing( uint256 _time ) internal virtual { @@ -343,6 +405,11 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { } } + /// @dev Advance time, create checkpoints, and rebalance at checkpoints. + /// @dev If time % _rebalanceInterval != 0 then it ends up advancing time + /// to the next _rebalanceInterval. + /// @param _time Amount of time to advance. + /// @param _options Rebalance options to pass to Everlong. function advanceTimeWithCheckpointsAndRebalancing( uint256 _time, IEverlong.RebalanceOptions memory _options @@ -380,19 +447,4 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { console.log("--------------------------------------------"); /* solhint-enable no-console */ } - - /// @dev Asserts that the position at the specified index is equal - /// to the input `position`. - /// @param _index Index of the position to compare. - /// @param _position Input position to validate against - /// @param _error Message to display for failing assertions. - function assertPosition( - uint256 _index, - IEverlong.Position memory _position, - string memory _error - ) public view virtual { - IEverlong.Position memory p = everlong.positionAt(_index); - assertEq(_position.maturityTime, p.maturityTime, _error); - assertEq(_position.bondAmount, p.bondAmount, _error); - } } diff --git a/test/integration/CloseImmatureLongs.t.sol b/test/integration/CloseImmatureLongs.t.sol index a6385f5..26fdc2d 100644 --- a/test/integration/CloseImmatureLongs.t.sol +++ b/test/integration/CloseImmatureLongs.t.sol @@ -144,7 +144,7 @@ contract CloseImmatureLongs is EverlongTest { uint256 basePaid = 10_000e18; ERC20Mintable(everlong.asset()).mint(basePaid); ERC20Mintable(everlong.asset()).approve(address(everlong), basePaid); - uint256 shares = depositEverlong(basePaid, bob); + uint256 shares = depositEverlong(basePaid, bob, true); // half term passes advanceTimeWithCheckpointsAndRebalancing(POSITION_DURATION / 2); @@ -155,7 +155,7 @@ contract CloseImmatureLongs is EverlongTest { console.log("totalAssets: %e", everlong.totalAssets()); // Close the long. - uint256 baseProceeds = redeemEverlong(shares, bob); + uint256 baseProceeds = redeemEverlong(shares, bob, true); console.log("actual: %s", baseProceeds); console.log( "assets: %s", @@ -197,13 +197,13 @@ contract CloseImmatureLongs is EverlongTest { ); // Ensure previewRedeem returns zero for a small amount of shares. - depositEverlong(_depositAmount, bob); + depositEverlong(_depositAmount, bob, true); _shareAmount = bound(_shareAmount, 0, 1000); uint256 assetsOwed = everlong.previewRedeem(_shareAmount); assertEq(assetsOwed, 0); // Ensure revert when attempting to redeem a small amount of shares. vm.expectRevert(IEverlong.RedemptionZeroOutput.selector); - redeemEverlong(_shareAmount, bob); + redeemEverlong(_shareAmount, bob, true); } } diff --git a/test/integration/PartialClosures.t.sol b/test/integration/PartialClosures.t.sol index 77af9fa..00b0e58 100644 --- a/test/integration/PartialClosures.t.sol +++ b/test/integration/PartialClosures.t.sol @@ -39,7 +39,7 @@ contract PartialClosures is EverlongTest { MINIMUM_TRANSACTION_AMOUNT * 100, hyperdrive.calculateMaxLong() ); - uint256 aliceShares = depositEverlong(aliceDepositAmount, alice); + uint256 aliceShares = depositEverlong(aliceDepositAmount, alice, true); uint256 positionBondsAfterDeposit = everlong.totalBonds(); // Alice redeems a significant enough portion of her shares to require @@ -50,7 +50,7 @@ contract PartialClosures is EverlongTest { 0.8e18 ); uint256 aliceRedeemAmount = aliceShares.mulDown(_redemptionPercentage); - redeemEverlong(aliceRedeemAmount, alice, false); + redeemEverlong(aliceRedeemAmount, alice); uint256 positionBondsAfterRedeem = everlong.totalBonds(); // Ensure Everlong still has a position open. @@ -89,7 +89,7 @@ contract PartialClosures is EverlongTest { // Alice deposits into Everlong. uint256 aliceDepositAmount = 10_000e18; - uint256 aliceShares = depositEverlong(aliceDepositAmount, alice); + uint256 aliceShares = depositEverlong(aliceDepositAmount, alice, true); // Time advances towards the end of the term. advanceTimeWithCheckpointsAndRebalancing( @@ -97,7 +97,7 @@ contract PartialClosures is EverlongTest { ); // Alice deposits again into Everlong. - aliceShares += depositEverlong(aliceDepositAmount, alice); + aliceShares += depositEverlong(aliceDepositAmount, alice, true); // Ensure Everlong has two positions and that the bond prices differ // by greater than Everlong's max closeLong slippage. @@ -120,7 +120,7 @@ contract PartialClosures is EverlongTest { // This should succeed. uint256 redeemPercentage = 0.75e18; uint256 aliceRedeemAmount = aliceShares.mulDown(redeemPercentage); - redeemEverlong(aliceRedeemAmount, alice); + redeemEverlong(aliceRedeemAmount, alice, true); // Ensure Everlong has one position left. assertEq(everlong.positionCount(), 1); diff --git a/test/integration/Sandwich.t.sol b/test/integration/Sandwich.t.sol index f3dfdb9..a75ba51 100644 --- a/test/integration/Sandwich.t.sol +++ b/test/integration/Sandwich.t.sol @@ -103,7 +103,8 @@ contract Sandwich is EverlongTest { ); uint256 bystanderShares = depositEverlong( _bystanderDepositAmount, - bystander + bystander, + true ); // The attacker opens a large short. @@ -125,7 +126,8 @@ contract Sandwich is EverlongTest { ); uint256 attackerShares = depositEverlong( _attackerDepositAmount, - attacker + attacker, + true ); // The attacker closes their short position. @@ -138,11 +140,12 @@ contract Sandwich is EverlongTest { // The attacker redeems their Everlong shares. uint256 attackerEverlongProceeds = redeemEverlong( attackerShares, - attacker + attacker, + true ); // The bystander redeems their Everlong shares. - redeemEverlong(bystanderShares, bystander); + redeemEverlong(bystanderShares, bystander, true); // Calculate the amount paid and the proceeds for the attacker. uint256 attackerPaid = _attackerDepositAmount + attackerShortBasePaid; @@ -187,7 +190,8 @@ contract Sandwich is EverlongTest { ); uint256 bystanderEverlongShares = depositEverlong( _bystanderDeposit, - bystander + bystander, + true ); // The attacker deposits into Everlong. @@ -198,7 +202,8 @@ contract Sandwich is EverlongTest { ); uint256 attackerEverlongShares = depositEverlong( _attackerDeposit, - attacker + attacker, + true ); // The attacker removes liquidity from Hyperdrive. @@ -207,7 +212,8 @@ contract Sandwich is EverlongTest { // The attacker redeems from Everlong. uint256 attackerEverlongProceeds = redeemEverlong( attackerEverlongShares, - attacker + attacker, + true ); // The bystander redeems from Everlong. @@ -215,7 +221,7 @@ contract Sandwich is EverlongTest { // While not needed for the assertion below, it's included to ensure // that the attack does not prevent the bystander from redeeming their // shares. - redeemEverlong(bystanderEverlongShares, bystander); + redeemEverlong(bystanderEverlongShares, bystander, true); // Ensure that the attacker does not profit from their actions. assertLt(attackerEverlongProceeds, _attackerDeposit); diff --git a/test/integration/VaultSharePriceManipulation.t.sol b/test/integration/VaultSharePriceManipulation.t.sol index d271619..577d23f 100644 --- a/test/integration/VaultSharePriceManipulation.t.sol +++ b/test/integration/VaultSharePriceManipulation.t.sol @@ -385,10 +385,10 @@ contract VaultSharePriceManipulation is EverlongTest { } // Initial deposit is made into everlong. - depositEverlong(params.initialDeposit, celine); + depositEverlong(params.initialDeposit, celine, true); // Innocent bystander deposits into everlong. - depositEverlong(params.bystanderDeposit, alice); + depositEverlong(params.bystanderDeposit, alice, true); // Attacker opens a short on hyperdrive. uint256 bobShortMaturityTime; @@ -404,13 +404,14 @@ contract VaultSharePriceManipulation is EverlongTest { // Attacker deposits into everlong. uint256 bobEverlongShares = depositEverlong( params.sandwichDeposit, - bob + bob, + true ); if (params.timeToCloseShort > 0) { advanceTimeWithCheckpointsAndRebalancing(params.timeToCloseShort); if (everlong.canRebalance()) { - everlong.rebalance(); + everlong.rebalance(DEFAULT_REBALANCE_OPTIONS); } } @@ -430,15 +431,14 @@ contract VaultSharePriceManipulation is EverlongTest { params.timeToCloseEverlong ); if (everlong.canRebalance()) { - everlong.rebalance(); + everlong.rebalance(DEFAULT_REBALANCE_OPTIONS); } } // Attacker redeems from everlong. - // uint256 bobProceedsEverlong = redeemEverlong(bobEverlongShares, bob); - redeemEverlong(bobEverlongShares, bob); + redeemEverlong(bobEverlongShares, bob, true); if (everlong.canRebalance()) { - everlong.rebalance(); + everlong.rebalance(DEFAULT_REBALANCE_OPTIONS); } if (params.bystanderCloseDelay > 0) { @@ -446,7 +446,7 @@ contract VaultSharePriceManipulation is EverlongTest { params.bystanderCloseDelay ); if (everlong.canRebalance()) { - everlong.rebalance(); + everlong.rebalance(DEFAULT_REBALANCE_OPTIONS); } } diff --git a/test/units/EverlongERC4626.t.sol b/test/units/EverlongERC4626.t.sol index 6b099c7..ba3ab7c 100644 --- a/test/units/EverlongERC4626.t.sol +++ b/test/units/EverlongERC4626.t.sol @@ -33,7 +33,7 @@ contract TestEverlongERC4626 is EverlongTest { // Deposit into everlong. uint256 amount = 250e18; - uint256 shares = depositEverlong(amount, alice); + uint256 shares = depositEverlong(amount, alice, true); // Ensure that previewRedeem output is at most equal to actual output // and within margins. @@ -48,7 +48,7 @@ contract TestEverlongERC4626 is EverlongTest { // Deposit into everlong. uint256 amount = 250e18; - uint256 shares = depositEverlong(amount, alice); + uint256 shares = depositEverlong(amount, alice, true); // Ensure that previewRedeem output is at most equal to actual output // and within margins. @@ -64,7 +64,7 @@ contract TestEverlongERC4626 is EverlongTest { // Deposit into everlong. uint256 amount = 250e18; - uint256 shares = depositEverlong(amount, alice); + uint256 shares = depositEverlong(amount, alice, true); // Fast forward to halfway through maturity. advanceTimeWithCheckpointsAndRebalancing(POSITION_DURATION / 2); @@ -83,7 +83,7 @@ contract TestEverlongERC4626 is EverlongTest { // Deposit into everlong. uint256 amount = 250e18; - uint256 shares = depositEverlong(amount, alice); + uint256 shares = depositEverlong(amount, alice, true); // Fast forward to halfway through maturity. advanceTimeWithCheckpointsAndRebalancing(POSITION_DURATION / 2); diff --git a/test/units/EverlongPortfolio.t.sol b/test/units/EverlongPortfolio.t.sol index 1c44711..8397878 100644 --- a/test/units/EverlongPortfolio.t.sol +++ b/test/units/EverlongPortfolio.t.sol @@ -24,7 +24,7 @@ contract TestEverlongPortfolio is EverlongTest { uint256 _index, IEverlong.Position memory _position, string memory _error - ) public view override { + ) public view { IEverlong.Position memory p = portfolio.at(_index); assertEq(_position.maturityTime, p.maturityTime, _error); assertEq(_position.bondAmount, p.bondAmount, _error); @@ -241,7 +241,7 @@ contract TestEverlongPortfolio is EverlongTest { // Attempt calling rebalance as Dan (not the admin). vm.startPrank(dan); vm.expectRevert(IEverlong.Unauthorized.selector); - everlong.rebalance(); + everlong.rebalance(DEFAULT_REBALANCE_OPTIONS); vm.stopPrank(); } From 7241846bbfaa885ec24d900cf30241c70bea2b1d Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Thu, 31 Oct 2024 16:40:20 -0500 Subject: [PATCH 15/16] rework spendingOverride into spendingLimit --- contracts/Everlong.sol | 62 +++++++++++++----------------- contracts/interfaces/IEverlong.sol | 12 +----- test/harnesses/EverlongTest.sol | 10 ++--- test/units/EverlongPortfolio.t.sol | 56 ++++++++------------------- 4 files changed, 49 insertions(+), 91 deletions(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index 8eac0cd..e11cee1 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -328,43 +328,33 @@ contract Everlong is IEverlong { // Calculate how much idle to spend on the position. uint256 balance = IERC20(_asset).balanceOf(address(this)); uint256 target = targetIdleLiquidity(); - uint256 toSpend = balance - target; - if (_options.spendingOverride != 0) { - // Spending override must not be less than Hyperdrive's - // minimum transaction amount. - if ( - _options.spendingOverride < - IHyperdrive(hyperdrive) - .getPoolConfig() - .minimumTransactionAmount - ) { - revert SpendingOverrideTooLow(); - } - - // Spending override must not be greater than the difference - // between Everlong's balance and the max idle liquidity. - if (_options.spendingOverride > toSpend) { - revert SpendingOverrideTooHigh(); - } - - // Apply the spending override. - toSpend = _options.spendingOverride; - } - - // Open a new position. Leave an extra wei for the approval to keep - // the slot warm. - IERC20(_asset).forceApprove(address(hyperdrive), toSpend + 1); - (uint256 maturityTime, uint256 bondAmount) = IHyperdrive(hyperdrive) - .openLong( - asBase, - toSpend, - _options.minOutput, - _options.minVaultSharePrice, - _options.extraData - ); + uint256 toSpend = ( + _options.spendingLimit == 0 + ? balance - target + : _options.spendingLimit.min(balance - target) + ); - // Account for the new position in the portfolio. - _portfolio.handleOpenPosition(maturityTime, bondAmount); + // If spending limit is above hyperdrive's minimum, open a new + // position. + // Leave an extra wei for the approval to keep the slot warm. + if ( + toSpend >= + IHyperdrive(hyperdrive).getPoolConfig().minimumTransactionAmount + ) { + IERC20(_asset).forceApprove(address(hyperdrive), toSpend + 1); + (uint256 maturityTime, uint256 bondAmount) = IHyperdrive( + hyperdrive + ).openLong( + asBase, + toSpend, + _options.minOutput, + _options.minVaultSharePrice, + _options.extraData + ); + + // Account for the new position in the portfolio. + _portfolio.handleOpenPosition(maturityTime, bondAmount); + } } // Calculate an updated portfolio value and save it. diff --git a/contracts/interfaces/IEverlong.sol b/contracts/interfaces/IEverlong.sol index f3041fc..28e2be3 100644 --- a/contracts/interfaces/IEverlong.sol +++ b/contracts/interfaces/IEverlong.sol @@ -31,7 +31,7 @@ abstract contract IEverlong is struct RebalanceOptions { /// @notice Limit on the amount of idle to spend on a new position. /// @dev A value of zero indicates no limit. - uint256 spendingOverride; + uint256 spendingLimit; /// @notice Minimum amount of bonds to receive when opening a position. uint256 minOutput; /// @notice Minimum vault share price when opening a position. @@ -77,14 +77,4 @@ abstract contract IEverlong is /// @notice Thrown when a redemption results in zero output assets. error RedemptionZeroOutput(); - - // ── Rebalancing ──────────────────────────────────────────── - - /// @notice Thrown when the spending override is below hyperdrive's - /// minimum transaction amount. - error SpendingOverrideTooLow(); - - /// @notice Thrown when the spending override is above the difference - /// between Everlong's balance and its max idle liquidity. - error SpendingOverrideTooHigh(); } diff --git a/test/harnesses/EverlongTest.sol b/test/harnesses/EverlongTest.sol index 723258e..5d26e9d 100644 --- a/test/harnesses/EverlongTest.sol +++ b/test/harnesses/EverlongTest.sol @@ -51,7 +51,7 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { IEverlong.RebalanceOptions internal DEFAULT_REBALANCE_OPTIONS = IEverlong.RebalanceOptions({ - spendingOverride: 0, + spendingLimit: 0, minOutput: 0, minVaultSharePrice: 0, positionClosureLimit: 0, @@ -133,7 +133,7 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { _depositor, false, IEverlong.RebalanceOptions({ - spendingOverride: 0, + spendingLimit: 0, minOutput: 0, minVaultSharePrice: 0, positionClosureLimit: 0, @@ -158,7 +158,7 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { _depositor, _shouldRebalance, IEverlong.RebalanceOptions({ - spendingOverride: 0, + spendingLimit: 0, minOutput: 0, minVaultSharePrice: 0, positionClosureLimit: 0, @@ -232,7 +232,7 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { _redeemer, true, IEverlong.RebalanceOptions({ - spendingOverride: 0, + spendingLimit: 0, minOutput: 0, minVaultSharePrice: 0, positionClosureLimit: 0, @@ -257,7 +257,7 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { _redeemer, _shouldRebalance, IEverlong.RebalanceOptions({ - spendingOverride: 0, + spendingLimit: 0, minOutput: 0, minVaultSharePrice: 0, positionClosureLimit: 0, diff --git a/test/units/EverlongPortfolio.t.sol b/test/units/EverlongPortfolio.t.sol index 8397878..5bb362e 100644 --- a/test/units/EverlongPortfolio.t.sol +++ b/test/units/EverlongPortfolio.t.sol @@ -302,7 +302,7 @@ contract TestEverlongPortfolio is EverlongTest { // Call rebalance with `positionClosureLimit` set to one. rebalance( IEverlong.RebalanceOptions({ - spendingOverride: 0, + spendingLimit: 0, minOutput: 0, minVaultSharePrice: 0, positionClosureLimit: 1, @@ -317,61 +317,39 @@ contract TestEverlongPortfolio is EverlongTest { assertTrue(everlong.positionAt(2).maturityTime > block.timestamp); } - /// @dev Tests the functionality of `RebalanceOptions.spendingOverride`. - function test_rebalance_options_spendingOverride() external { + /// @dev Tests the functionality of `RebalanceOptions.spendingLimit`. + function test_rebalance_options_spendingLimit() external { // Mint Everlong some assets. mintApproveEverlongBaseAsset(address(everlong), 10_000e18); - // Try rebalancing with too low of a `spendingOverride`. - vm.startPrank(everlong.admin()); - vm.expectRevert(IEverlong.SpendingOverrideTooLow.selector); - everlong.rebalance( + // Try rebalancing with too low of a `spendingLimit`. No positions + // should be created. + rebalance( IEverlong.RebalanceOptions({ - spendingOverride: 1, + spendingLimit: 1, minOutput: 0, minVaultSharePrice: 0, positionClosureLimit: 0, extraData: "" }) ); - vm.stopPrank(); + assertEq(everlong.positionCount(), 0); - // Try rebalancing with too high of a `spendingOverride`. - uint256 balance = IERC20(everlong.asset()).balanceOf(address(everlong)); + // Try rebalancing with too high of a `spendingLimit`. A position should + // be created and idle liquidity should be within 1% of target. uint256 targetIdle = everlong.targetIdleLiquidity(); - vm.startPrank(everlong.admin()); - vm.expectRevert(IEverlong.SpendingOverrideTooHigh.selector); - everlong.rebalance( - IEverlong.RebalanceOptions({ - spendingOverride: (balance - targetIdle) * 2, - minOutput: 0, - minVaultSharePrice: 0, - positionClosureLimit: 0, - extraData: "" - }) - ); - vm.stopPrank(); - - // Try rebalancing with an acceptable `spendingOverride`. - uint256 maxIdle = everlong.maxIdleLiquidity(); - vm.startPrank(everlong.admin()); - everlong.rebalance( + rebalance( IEverlong.RebalanceOptions({ - spendingOverride: (balance - maxIdle) / 2, + spendingLimit: type(uint256).max, minOutput: 0, minVaultSharePrice: 0, positionClosureLimit: 0, extraData: "" }) ); - vm.stopPrank(); - - // Ensure that after rebalancing, the idle liquidity is still greater - // than the maximum. - assertGt( - IERC20(everlong.asset()).balanceOf(address(everlong)), - everlong.maxIdleLiquidity() - ); + uint256 balance = IERC20(everlong.asset()).balanceOf(address(everlong)); + assertEq(everlong.positionCount(), 1); + assertApproxEqRel(balance, targetIdle, 0.01e18); } /// @dev Tests the functionality of `RebalanceOptions.minOutput`. @@ -383,7 +361,7 @@ contract TestEverlongPortfolio is EverlongTest { vm.expectRevert(); everlong.rebalance( IEverlong.RebalanceOptions({ - spendingOverride: 0, + spendingLimit: 0, minOutput: type(uint256).max, minVaultSharePrice: 0, positionClosureLimit: 0, @@ -401,7 +379,7 @@ contract TestEverlongPortfolio is EverlongTest { vm.expectRevert(); everlong.rebalance( IEverlong.RebalanceOptions({ - spendingOverride: 0, + spendingLimit: 0, minOutput: 0, minVaultSharePrice: type(uint256).max, positionClosureLimit: 0, From b2e7b1070608babcd0ff397156952740cd8006f4 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Thu, 31 Oct 2024 16:44:54 -0500 Subject: [PATCH 16/16] wording --- contracts/Everlong.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index e11cee1..3a6e65a 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -326,6 +326,7 @@ contract Everlong is IEverlong { // If Everlong has sufficient idle, open a new position. if (canOpenPosition()) { // Calculate how much idle to spend on the position. + // A value of 0 for spendingLimit indicates no limit. uint256 balance = IERC20(_asset).balanceOf(address(this)); uint256 target = targetIdleLiquidity(); uint256 toSpend = ( @@ -334,7 +335,7 @@ contract Everlong is IEverlong { : _options.spendingLimit.min(balance - target) ); - // If spending limit is above hyperdrive's minimum, open a new + // If toSpend is above hyperdrive's minimum, open a new // position. // Leave an extra wei for the approval to keep the slot warm. if (