From ea3f669f975b58c3ce9278b8ca49f2f85e30c53e Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Thu, 19 Dec 2024 07:15:54 -0600 Subject: [PATCH 1/3] adds the emergencyWithdraw override function to withdraw funds after strategy shutdown --- contracts/EverlongStrategy.sol | 27 ++++++ test/everlong/units/EmergencyWithdraw.t.sol | 96 +++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 test/everlong/units/EmergencyWithdraw.t.sol diff --git a/contracts/EverlongStrategy.sol b/contracts/EverlongStrategy.sol index 687f874..8dc29d2 100644 --- a/contracts/EverlongStrategy.sol +++ b/contracts/EverlongStrategy.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.24; import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; +import { IMultiToken } from "hyperdrive/contracts/src/interfaces/IMultiToken.sol"; +import { AssetId } from "hyperdrive/contracts/src/libraries/AssetId.sol"; import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol"; import { SafeCast } from "hyperdrive/contracts/src/libraries/SafeCast.sol"; import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; @@ -219,6 +221,31 @@ contract EverlongStrategy is BaseStrategy { return; } + /// @dev Withdraw function that can be called after the vault is shut down. + /// Takes all longs controlled by the strategy and transfers them to + /// the management address. + /// @param . Amount of assets to withdraw. This is ignored to reduce the + /// likelihood of reverts. + function _emergencyWithdraw(uint256) internal override { + IEverlongStrategy.EverlongPosition memory position; + while (!_portfolio.isEmpty()) { + // Retrieve the most mature position. + position = _portfolio.head(); + + // Transfer the tokens to the management address. + IMultiToken(hyperdrive).transferFrom( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + uint256(position.maturityTime) + ), + address(this), + TokenizedStrategy.emergencyAdmin(), + uint256(position.bondAmount) + ); + _portfolio.handleClosePosition(); + } + } + /// @dev Attempt to free the '_amount' of 'asset'. /// - Any difference between `_amount` and what is actually freed will be /// counted as a loss and passed on to the withdrawer. diff --git a/test/everlong/units/EmergencyWithdraw.t.sol b/test/everlong/units/EmergencyWithdraw.t.sol new file mode 100644 index 0000000..5e99143 --- /dev/null +++ b/test/everlong/units/EmergencyWithdraw.t.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import { IMultiToken } from "hyperdrive/contracts/src/interfaces/IMultiToken.sol"; +import { AssetId } from "hyperdrive/contracts/src/libraries/AssetId.sol"; +import { IEverlongStrategy } from "../../../contracts/interfaces/IEverlongStrategy.sol"; +import { EVERLONG_STRATEGY_KIND, EVERLONG_VERSION } from "../../../contracts/libraries/Constants.sol"; +import { EverlongTest } from "../EverlongTest.sol"; + +/// @dev Tests emergency withdraw functionality. +contract TestEmergencyWithdraw is EverlongTest { + function test_call_from_non_management_failure() external { + // Shut down the strategy. + vm.startPrank(strategy.emergencyAdmin()); + strategy.shutdownStrategy(); + vm.stopPrank(); + + // Ensure calling emergencyWithdraw from a random address fails. + vm.startPrank(alice); + vm.expectRevert(); + strategy.emergencyWithdraw(0); + vm.stopPrank(); + + // Ensure calling emergencyWithdraw from the keeper address fails. + vm.startPrank(keeper); + vm.expectRevert(); + strategy.emergencyWithdraw(0); + vm.stopPrank(); + + // Ensure calling emergencyWithdraw from the keeper contract address + // fails. + vm.startPrank(address(keeperContract)); + vm.expectRevert(); + strategy.emergencyWithdraw(0); + vm.stopPrank(); + } + + /// @dev Ensure strategy can be shutdown when it has no positions. + function test_no_positions_open() external { + // Ensure the strategy has no open positions. + assertEq(IEverlongStrategy(address(strategy)).positionCount(), 0); + + // Shut down the strategy and call `emergencyWithdraw`. + vm.startPrank(strategy.emergencyAdmin()); + strategy.shutdownStrategy(); + strategy.emergencyWithdraw(0); + vm.stopPrank(); + } + + /// @dev Ensure strategy can be shutdown when it has positions. + function test_positions_open() external { + // Deposit into the vault and "rebalance" to open a position in the + // strategy. + depositVault(100e18, alice, true); + rebalance(); + + // Ensure the strategy has one open position. + assertEq(IEverlongStrategy(address(strategy)).positionCount(), 1); + + // Get the position. + IEverlongStrategy.EverlongPosition memory position = IEverlongStrategy( + address(strategy) + ).positionAt(0); + + // Record the strategy's balance of longs for that position. + uint256 strategyLongBalance = IMultiToken(hyperdrive).balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + uint256(position.maturityTime) + ), + address(strategy) + ); + + // Shut down the strategy and call `emergencyWithdraw`. + vm.startPrank(strategy.emergencyAdmin()); + strategy.shutdownStrategy(); + strategy.emergencyWithdraw(0); + vm.stopPrank(); + + // Ensure the emergency admin address's long balance matches the strategy's + // long balance prior to the emergency withdraw. + assertEq( + strategyLongBalance, + IMultiToken(hyperdrive).balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + uint256(position.maturityTime) + ), + address(strategy.emergencyAdmin()) + ) + ); + + // Ensure the strategy has no positions left. + assertEq(IEverlongStrategy(address(strategy)).positionCount(), 0); + } +} From a69e210a37c96768544928ad63e2f97a83857196 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Thu, 19 Dec 2024 07:17:04 -0600 Subject: [PATCH 2/3] comment wording fix --- contracts/EverlongStrategy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/EverlongStrategy.sol b/contracts/EverlongStrategy.sol index 8dc29d2..4051ae2 100644 --- a/contracts/EverlongStrategy.sol +++ b/contracts/EverlongStrategy.sol @@ -223,7 +223,7 @@ contract EverlongStrategy is BaseStrategy { /// @dev Withdraw function that can be called after the vault is shut down. /// Takes all longs controlled by the strategy and transfers them to - /// the management address. + /// the emergency admin address. /// @param . Amount of assets to withdraw. This is ignored to reduce the /// likelihood of reverts. function _emergencyWithdraw(uint256) internal override { From 4e5b89036371fd36e29fd3335b9b83a490a3d23b Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Tue, 7 Jan 2025 05:38:02 -0600 Subject: [PATCH 3/3] addressing feedback from @mrtoph Use the input `uint256` in `_emergencyWithdraw` as a limit on the maximum number of bonds to be transferred. Add tests to ensure it works. --- contracts/EverlongStrategy.sol | 22 ++++-- test/everlong/units/EmergencyWithdraw.t.sol | 79 +++++++++++++++++++-- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/contracts/EverlongStrategy.sol b/contracts/EverlongStrategy.sol index 4051ae2..9dc49b3 100644 --- a/contracts/EverlongStrategy.sol +++ b/contracts/EverlongStrategy.sol @@ -222,16 +222,28 @@ contract EverlongStrategy is BaseStrategy { } /// @dev Withdraw function that can be called after the vault is shut down. - /// Takes all longs controlled by the strategy and transfers them to - /// the emergency admin address. - /// @param . Amount of assets to withdraw. This is ignored to reduce the - /// likelihood of reverts. - function _emergencyWithdraw(uint256) internal override { + /// Transfers bond positions to the `emergencyAdmin` address until + /// a position's transfer would exceed `_maxBondAmount` or there are + /// no longer any bond positions under the strategy's control. + /// @param _maxBondAmount Maximum amount of bonds to transfer from positions. + function _emergencyWithdraw(uint256 _maxBondAmount) internal override { IEverlongStrategy.EverlongPosition memory position; while (!_portfolio.isEmpty()) { // Retrieve the most mature position. position = _portfolio.head(); + // If transferring the position's bonds would cause the total amount + // transferred to exceed `_maxBondAmount` then exit. + if (position.bondAmount > _maxBondAmount) { + return; + } + // Deduct the position's bonds from `_maxBondAmount` so that it + // tracks how many more bonds can be transferred before hitting the + // limit. + else { + _maxBondAmount -= position.bondAmount; + } + // Transfer the tokens to the management address. IMultiToken(hyperdrive).transferFrom( AssetId.encodeAssetId( diff --git a/test/everlong/units/EmergencyWithdraw.t.sol b/test/everlong/units/EmergencyWithdraw.t.sol index 5e99143..ea6b0cd 100644 --- a/test/everlong/units/EmergencyWithdraw.t.sol +++ b/test/everlong/units/EmergencyWithdraw.t.sol @@ -18,20 +18,20 @@ contract TestEmergencyWithdraw is EverlongTest { // Ensure calling emergencyWithdraw from a random address fails. vm.startPrank(alice); vm.expectRevert(); - strategy.emergencyWithdraw(0); + strategy.emergencyWithdraw(type(uint256).max); vm.stopPrank(); // Ensure calling emergencyWithdraw from the keeper address fails. vm.startPrank(keeper); vm.expectRevert(); - strategy.emergencyWithdraw(0); + strategy.emergencyWithdraw(type(uint256).max); vm.stopPrank(); // Ensure calling emergencyWithdraw from the keeper contract address // fails. vm.startPrank(address(keeperContract)); vm.expectRevert(); - strategy.emergencyWithdraw(0); + strategy.emergencyWithdraw(type(uint256).max); vm.stopPrank(); } @@ -43,7 +43,7 @@ contract TestEmergencyWithdraw is EverlongTest { // Shut down the strategy and call `emergencyWithdraw`. vm.startPrank(strategy.emergencyAdmin()); strategy.shutdownStrategy(); - strategy.emergencyWithdraw(0); + strategy.emergencyWithdraw(type(uint256).max); vm.stopPrank(); } @@ -74,7 +74,76 @@ contract TestEmergencyWithdraw is EverlongTest { // Shut down the strategy and call `emergencyWithdraw`. vm.startPrank(strategy.emergencyAdmin()); strategy.shutdownStrategy(); - strategy.emergencyWithdraw(0); + strategy.emergencyWithdraw(type(uint256).max); + vm.stopPrank(); + + // Ensure the emergency admin address's long balance matches the strategy's + // long balance prior to the emergency withdraw. + assertEq( + strategyLongBalance, + IMultiToken(hyperdrive).balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + uint256(position.maturityTime) + ), + address(strategy.emergencyAdmin()) + ) + ); + + // Ensure the strategy has no positions left. + assertEq(IEverlongStrategy(address(strategy)).positionCount(), 0); + } + + function test_maxBondAmount() external { + // Deposit into the vault and "rebalance" to open a position in the + // strategy. + depositVault(100e18, alice, true); + rebalance(); + + // Ensure the strategy has one open position. + assertEq(IEverlongStrategy(address(strategy)).positionCount(), 1); + + // Get the position. + IEverlongStrategy.EverlongPosition memory position = IEverlongStrategy( + address(strategy) + ).positionAt(0); + + // Record the strategy's balance of longs for that position. + uint256 strategyLongBalance = IMultiToken(hyperdrive).balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + uint256(position.maturityTime) + ), + address(strategy) + ); + + // Shut down the strategy and call `emergencyWithdraw` with + // `_maxBondAmount` set to a value less than the position's bond amount. + vm.startPrank(strategy.emergencyAdmin()); + strategy.shutdownStrategy(); + strategy.emergencyWithdraw(strategyLongBalance - 1); + vm.stopPrank(); + + // Ensure the emergency admin address's long balance is zero since no + // positions were transferred. + assertEq( + 0, + IMultiToken(hyperdrive).balanceOf( + AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + uint256(position.maturityTime) + ), + address(strategy.emergencyAdmin()) + ) + ); + + // Ensure the strategy still has one open position. + assertEq(IEverlongStrategy(address(strategy)).positionCount(), 1); + + // Call `emergencyWithdraw` with `_maxBondAmount` set to a value more + // than the position's bond amount. + vm.startPrank(strategy.emergencyAdmin()); + strategy.emergencyWithdraw(strategyLongBalance + 1); vm.stopPrank(); // Ensure the emergency admin address's long balance matches the strategy's