diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index 3a6e65a..edb2419 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -98,7 +98,7 @@ contract Everlong is IEverlong { /// @notice Maximum slippage allowed when closing longs with Hyperdrive. /// @dev Represented as a percentage with 1e18 signifying 100%. - uint256 public constant maxCloseLongSlippage = 0.001e18; + uint256 public constant maxCloseLongSlippage = 0.0001e18; /// @notice Amount of additional bonds to close during a partial position /// closure to avoid rounding errors. Represented as a percentage @@ -236,6 +236,8 @@ contract Everlong is IEverlong { /// @notice Returns an approximate lower bound on the amount of assets /// received from redeeming the specified amount of shares. + /// @dev Losses and gains to the portfolio since the last `_totalAssets` + /// update are applied proportionally. /// @param _shares Amount of shares to redeem. /// @return assets Amount of assets that will be received. function previewRedeem( @@ -244,36 +246,28 @@ contract Everlong is IEverlong { // Convert the share amount to assets. assets = convertToAssets(_shares); - // TODO: Hold the vault share price constant. - // - // Apply losses incurred by the portfolio. - uint256 losses = _calculatePortfolioLosses().mulDivUp( - assets, - _totalAssets - ); - - // If the losses from closing immature positions exceeds the assets - // owed to the redeemer, set the assets owed to zero. - if (losses > assets) { - // NOTE: We return zero since `previewRedeem` must not revert. - assets = 0; - } - // Decrement the assets owed to the redeemer by the amount of losses - // incurred from closing immature positions. - else { + // Calculate and apply unrealized portfolio losses. + // Losses are rounded up. + uint256 lastTotalAssets = _totalAssets; + uint256 currentTotalAssets = _calculateTotalAssets(); + if (_totalAssets > currentTotalAssets) { unchecked { - assets -= losses; + assets -= (lastTotalAssets - currentTotalAssets).mulDivUp( + assets, + lastTotalAssets + ); } } } - /// @dev Attempt rebalancing after a deposit if idle is above max. + /// @dev Increase Everlong's total assets by the amount deposited. function _afterDeposit(uint256 _assets, uint256) internal virtual override { // Add the deposit to Everlong's assets. _totalAssets += _assets; } - /// @dev Frees sufficient assets for a withdrawal by closing positions. + /// @dev Frees sufficient assets for a withdrawal by closing positions and + /// update Everlong's total assets accounting. /// @param _assets Amount of assets owed to the withdrawer. function _beforeWithdraw( uint256 _assets, @@ -297,9 +291,8 @@ contract Everlong is IEverlong { _closePositions(_assets - balance); } - // Recalculate the assets under Everlong control less the amount being - // withdrawn. - _totalAssets = _calculateTotalAssets() - _assets; + // Decrement the assets under Everlong control by withdrawal amount. + _totalAssets -= _assets; } // ╭─────────────────────────────────────────────────────────╮ @@ -467,10 +460,17 @@ contract Everlong is IEverlong { // Update portfolio accounting to reflect the closed position. _portfolio.handleClosePosition(); + + // Increment the counter tracking the number of positions closed. + ++count; } } /// @dev Close positions until the targeted amount of output is received. + /// @dev It is possible for this function to successfully return without + /// having received the target amount of assets. In practice, this + /// does not occur due to slippage guards and underestimation + /// of Everlong's portfolio value. /// @param _targetOutput Target amount of proceeds to receive. /// @return output Total output received from closed positions. function _closePositions( @@ -502,6 +502,7 @@ contract Everlong is IEverlong { // Close only part of the position if there are sufficient bonds // to reach the target output without leaving a small amount left. + // // For this case, the remaining bonds must be worth at least // Hyperdrive's minimum transaction amount. if ( @@ -567,29 +568,19 @@ 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 - }), - "" - ); - } - } - - /// @dev Calculates the amount of losses the portfolio has incurred since - /// `_totalAssets` was last calculated. If no losses have been incurred - /// return 0. - /// @return Amount of losses incurred by the portfolio (if any). - function _calculatePortfolioLosses() internal view returns (uint256) { - uint256 newTotalAssets = _calculateTotalAssets(); - if (_totalAssets > newTotalAssets) { - return _totalAssets - newTotalAssets; + value += IHyperdrive(hyperdrive) + .previewCloseLong( + asBase, + IEverlong.Position({ + maturityTime: IHyperdrive(hyperdrive) + .getCheckpointIdUp(_portfolio.avgMaturityTime) + .toUint128(), + bondAmount: _portfolio.totalBonds + }), + "" + ) + .mulDown(1e18 - maxCloseLongSlippage); } - return 0; } // ╭─────────────────────────────────────────────────────────╮ diff --git a/contracts/interfaces/IEverlong.sol b/contracts/interfaces/IEverlong.sol index 28e2be3..db3f7a9 100644 --- a/contracts/interfaces/IEverlong.sol +++ b/contracts/interfaces/IEverlong.sol @@ -77,4 +77,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 1895de8..18f92c6 100644 --- a/contracts/interfaces/IEverlongPortfolio.sol +++ b/contracts/interfaces/IEverlongPortfolio.sol @@ -19,6 +19,13 @@ interface IEverlongPortfolio { uint256 _limit ) external returns (uint256 output); + /// @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 │ // ╰─────────────────────────────────────────────────────────╯ diff --git a/test/integration/CloseImmatureLongs.t.sol b/test/integration/CloseImmatureLongs.t.sol index 26fdc2d..423253e 100644 --- a/test/integration/CloseImmatureLongs.t.sol +++ b/test/integration/CloseImmatureLongs.t.sol @@ -15,7 +15,7 @@ uint256 constant HYPERDRIVE_LONG_EXPOSURE_LONGS_OUTSTANDING_SLOT = 3; uint256 constant HYPERDRIVE_SHARE_ADJUSTMENT_SHORTS_OUTSTANDING_SLOT = 4; /// @dev Tests pricing functionality for the portfolio and unmatured positions. -contract CloseImmatureLongs is EverlongTest { +contract TestCloseImmatureLongs is EverlongTest { using Packing for bytes32; using FixedPointMath for uint128; using FixedPointMath for uint256; diff --git a/test/integration/PartialClosures.t.sol b/test/integration/PartialClosures.t.sol index 00b0e58..2cef438 100644 --- a/test/integration/PartialClosures.t.sol +++ b/test/integration/PartialClosures.t.sol @@ -9,7 +9,7 @@ import { EverlongTest } from "../harnesses/EverlongTest.sol"; import { IEverlong } from "../../contracts/interfaces/IEverlong.sol"; import { HyperdriveExecutionLibrary } from "../../contracts/libraries/HyperdriveExecution.sol"; -contract PartialClosures is EverlongTest { +contract TestPartialClosures is EverlongTest { using FixedPointMath for uint256; using Lib for *; using HyperdriveUtils for *; @@ -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. @@ -64,7 +64,7 @@ contract PartialClosures is EverlongTest { 1e18 - TARGET_IDLE_LIQUIDITY_PERCENTAGE ), positionBondsAfterRedeem, - 0.01e18 + 0.001e18 ); } diff --git a/test/integration/Sandwich.t.sol b/test/integration/Sandwich.t.sol index a75ba51..91aa24c 100644 --- a/test/integration/Sandwich.t.sol +++ b/test/integration/Sandwich.t.sol @@ -6,7 +6,7 @@ import { Lib } from "hyperdrive/test/utils/Lib.sol"; import { HyperdriveUtils } from "hyperdrive/test/utils/HyperdriveUtils.sol"; import { EverlongTest } from "../harnesses/EverlongTest.sol"; -contract Sandwich is EverlongTest { +contract TestSandwich is EverlongTest { using Lib for *; using HyperdriveUtils for *; @@ -75,6 +75,8 @@ contract Sandwich is EverlongTest { } // TODO: Decrease min range to Hyperdrive `MINIMUM_TRANSACTION_AMOUNT`. + // NOTE: Rebalancing is not performed after some interactions with Everlong + // since the attack being evaluated is atomic. // /// @dev Tests the following scenario: /// 1. First an innocent bystander deposits into Everlong. At that time, we @@ -95,6 +97,9 @@ contract Sandwich is EverlongTest { address attacker = alice; address bystander = bob; + // Initialize Everlong with a bond portfolio. + depositEverlong(10_000e18, celine); + // The bystander deposits into Everlong. _bystanderDepositAmount = bound( _bystanderDepositAmount, @@ -153,10 +158,12 @@ contract Sandwich is EverlongTest { attackerShortProceeds; // Ensure the attacker does not profit. - assertLt(attackerProceeds, attackerPaid); + assertLe(attackerProceeds, attackerPaid); } // TODO: Decrease min range to Hyperdrive `MINIMUM_TRANSACTION_AMOUNT`. + // NOTE: Rebalancing is not performed after some interactions with Everlong + // since the attack being evaluated is atomic. // /// @dev Tests the following scenario: /// 1. Attacker adds liquidity. @@ -224,6 +231,6 @@ contract Sandwich is EverlongTest { redeemEverlong(bystanderEverlongShares, bystander, true); // Ensure that the attacker does not profit from their actions. - assertLt(attackerEverlongProceeds, _attackerDeposit); + assertLe(attackerEverlongProceeds, _attackerDeposit); } } diff --git a/test/playgrounds/VaultSharePrice.t.sol b/test/playgrounds/VaultSharePrice.t.sol new file mode 100644 index 0000000..264c798 --- /dev/null +++ b/test/playgrounds/VaultSharePrice.t.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.22; + +import { console2 as console } from "forge-std/console2.sol"; +import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol"; +import { Lib } from "hyperdrive/test/utils/Lib.sol"; +import { HyperdriveUtils } from "hyperdrive/test/utils/HyperdriveUtils.sol"; +import { IERC20 } from "openzeppelin/interfaces/IERC20.sol"; +import { EverlongTest } from "../harnesses/EverlongTest.sol"; +import { IEverlong } from "../../contracts/interfaces/IEverlong.sol"; +import { HyperdriveExecutionLibrary } from "../../contracts/libraries/HyperdriveExecution.sol"; + +contract TestVaultSharePrice is EverlongTest { + using FixedPointMath for uint256; + using Lib for *; + using HyperdriveUtils for *; + using HyperdriveExecutionLibrary for *; + + function test_vault_share_price_deposit_redeem() external { + // Skip this test unless disabled manually. + vm.skip(true); + + deployEverlong(); + + // Alice makes a deposit. + uint256 aliceDeposit = 10_000e18; + uint256 aliceShares = depositEverlong(aliceDeposit, alice); + + console.log( + "Vault Share Price 1: %e", + everlong.totalAssets().divDown(everlong.totalSupply()) + ); + + // Bob makes a deposit. + uint256 bobDeposit = 10_000e18; + uint256 bobShares = depositEverlong(bobDeposit, bob); + console.log( + "Vault Share Price 2: %e", + everlong.totalAssets().divDown(everlong.totalSupply()) + ); + + // Celine makes a deposit. + uint256 celineDeposit = 10_000e18; + uint256 celineShares = depositEverlong(celineDeposit, celine); + console.log( + "Vault Share Price 3: %e", + everlong.totalAssets().divDown(everlong.totalSupply()) + ); + + // Bob redeems. + redeemEverlong(bobShares, bob); + console.log( + "Vault Share Price 4: %e", + everlong.totalAssets().divDown(everlong.totalSupply()) + ); + + // Celine redeems. + redeemEverlong(celineShares, celine); + console.log( + "Vault Share Price 5: %e", + everlong.totalAssets().divDown(everlong.totalSupply()) + ); + + console.log( + "Everlong Balance: %e", + IERC20(everlong.asset()).balanceOf(address(everlong)) + ); + } +} diff --git a/test/integration/VaultSharePriceManipulation.t.sol b/test/playgrounds/VaultSharePriceManipulation.t.sol similarity index 99% rename from test/integration/VaultSharePriceManipulation.t.sol rename to test/playgrounds/VaultSharePriceManipulation.t.sol index 577d23f..987b820 100644 --- a/test/integration/VaultSharePriceManipulation.t.sol +++ b/test/playgrounds/VaultSharePriceManipulation.t.sol @@ -18,7 +18,7 @@ uint256 constant HYPERDRIVE_LONG_EXPOSURE_LONGS_OUTSTANDING_SLOT = 3; uint256 constant HYPERDRIVE_SHARE_ADJUSTMENT_SHORTS_OUTSTANDING_SLOT = 4; /// @dev Tests vault share price manipulation with the underlying hyperdrive instance. -contract VaultSharePriceManipulation is EverlongTest { +contract TestVaultSharePriceManipulation is EverlongTest { using Packing for bytes32; using FixedPointMath for uint128; using FixedPointMath for uint256; diff --git a/test/units/EverlongERC4626.t.sol b/test/units/EverlongERC4626.t.sol index ba3ab7c..e67482e 100644 --- a/test/units/EverlongERC4626.t.sol +++ b/test/units/EverlongERC4626.t.sol @@ -92,18 +92,4 @@ contract TestEverlongERC4626 is EverlongTest { // and within margins. assertRedemption(shares / 3, alice); } - - /// @dev Tests that the `_beforeWithdraw` hook doesn't underflow when - /// Everlong's balance is greater than the assets being redeemed. - function test_beforeWithdraw_balance_gt_assets() external { - // Deploy Everlong. - deployEverlong(); - - // Mint some assets to everlong - uint256 assets = 100e18; - mintApproveEverlongBaseAsset(address(everlong), assets); - - // Call the `_beforeWithdraw` hook. - everlong.exposed_beforeWithdraw(assets - 10e18, 0); - } } diff --git a/test/units/EverlongPortfolio.t.sol b/test/units/EverlongPortfolio.t.sol index 5bb362e..be1e018 100644 --- a/test/units/EverlongPortfolio.t.sol +++ b/test/units/EverlongPortfolio.t.sol @@ -329,7 +329,7 @@ contract TestEverlongPortfolio is EverlongTest { spendingLimit: 1, minOutput: 0, minVaultSharePrice: 0, - positionClosureLimit: 0, + positionClosureLimit: 1, extraData: "" }) );