From 7ac7bd511239c1c82a8092b5b3eb4be866240e1d Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Wed, 7 Aug 2024 05:13:28 -0500 Subject: [PATCH 01/27] Very simple deposit/withdraw logic and share accounting --- contracts/Everlong.sol | 4 +- contracts/internal/EverlongAdmin.sol | 4 +- contracts/internal/EverlongBase.sol | 29 +---- contracts/internal/EverlongERC4626.sol | 105 ++++++++++------ contracts/internal/EverlongPositions.sol | 31 ++++- contracts/internal/EverlongStorage.sol | 68 ++++++++++ test/exposed/EverlongBaseExposed.sol | 14 +++ test/exposed/EverlongERC4626Exposed.sol | 19 +++ test/exposed/EverlongExposed.sol | 13 +- test/harnesses/EverlongTest.sol | 8 +- test/units/EverlongERC4626.t.sol | 152 +++++++++++++++++++++++ 11 files changed, 364 insertions(+), 83 deletions(-) create mode 100644 contracts/internal/EverlongStorage.sol create mode 100644 test/exposed/EverlongBaseExposed.sol create mode 100644 test/exposed/EverlongERC4626Exposed.sol create mode 100644 test/units/EverlongERC4626.t.sol diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index 1019eea..4364356 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.20; -import { EverlongAdmin } from "./internal/EverlongAdmin.sol"; import { EverlongBase } from "./internal/EverlongBase.sol"; -import { EverlongPositions } from "./internal/EverlongPositions.sol"; /// ,---..-. .-.,---. ,---. ,-. .---. .-. .-. ,--, /// | .-' \ \ / / | .-' | .-.\ | | / .-. ) | \| |.' .' @@ -62,7 +60,7 @@ import { EverlongPositions } from "./internal/EverlongPositions.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -contract Everlong is EverlongAdmin, EverlongPositions { +contract Everlong is EverlongBase { /// @notice Initial configuration paramters for Everlong. /// @param _name Name of the ERC20 token managed by Everlong. /// @param _symbol Symbol of the ERC20 token managed by Everlong. diff --git a/contracts/internal/EverlongAdmin.sol b/contracts/internal/EverlongAdmin.sol index dc0dead..f5e12a0 100644 --- a/contracts/internal/EverlongAdmin.sol +++ b/contracts/internal/EverlongAdmin.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.20; import { IEverlong } from "../interfaces/IEverlong.sol"; import { IEverlongAdmin } from "../interfaces/IEverlongAdmin.sol"; -import { EverlongBase } from "./EverlongBase.sol"; +import { EverlongStorage } from "./EverlongStorage.sol"; /// @author DELV /// @title EverlongAdmin @@ -11,7 +11,7 @@ import { EverlongBase } from "./EverlongBase.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -abstract contract EverlongAdmin is EverlongBase, IEverlongAdmin { +abstract contract EverlongAdmin is EverlongStorage, IEverlongAdmin { // ╭─────────────────────────────────────────────────────────╮ // │ Modifiers │ // ╰─────────────────────────────────────────────────────────╯ diff --git a/contracts/internal/EverlongBase.sol b/contracts/internal/EverlongBase.sol index 12c40b7..418728d 100644 --- a/contracts/internal/EverlongBase.sol +++ b/contracts/internal/EverlongBase.sol @@ -4,8 +4,8 @@ pragma solidity 0.8.20; import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; import { DoubleEndedQueue } from "openzeppelin/utils/structs/DoubleEndedQueue.sol"; import { IERC20 } from "openzeppelin/interfaces/IERC20.sol"; -import { IEverlongEvents } from "../interfaces/IEverlongEvents.sol"; import { EVERLONG_KIND, EVERLONG_VERSION } from "../libraries/Constants.sol"; +import { EverlongAdmin } from "./EverlongAdmin.sol"; import { EverlongERC4626 } from "./EverlongERC4626.sol"; // TODO: Reassess whether centralized configuration management makes sense. @@ -16,34 +16,9 @@ import { EverlongERC4626 } from "./EverlongERC4626.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -abstract contract EverlongBase is EverlongERC4626, IEverlongEvents { +abstract contract EverlongBase is EverlongAdmin, EverlongERC4626 { using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; - // ╭─────────────────────────────────────────────────────────╮ - // │ Storage │ - // ╰─────────────────────────────────────────────────────────╯ - - // Admin // - - /// @dev Address of the contract admin. - address internal _admin; - - // Hyperdrive // - - /// @dev Address of the Hyperdrive instance wrapped by Everlong. - address public immutable hyperdrive; - - /// @dev Whether to use Hyperdrive's base token to purchase bonds. - // If false, use the Hyperdrive's `vaultSharesToken`. - bool internal immutable _asBase; - - // Positions // - - // TODO: Reassess using a more tailored data structure. - /// @dev Utility data structure to manage the position queue. - /// Supports pushing and popping from both the front and back. - DoubleEndedQueue.Bytes32Deque internal _positions; - // ╭─────────────────────────────────────────────────────────╮ // │ Constructor │ // ╰─────────────────────────────────────────────────────────╯ diff --git a/contracts/internal/EverlongERC4626.sol b/contracts/internal/EverlongERC4626.sol index 40d17e1..d8466c0 100644 --- a/contracts/internal/EverlongERC4626.sol +++ b/contracts/internal/EverlongERC4626.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.20; +import { IERC20 } from "openzeppelin/interfaces/IERC20.sol"; import { ERC4626 } from "solady/tokens/ERC4626.sol"; +import { EverlongPositions } from "./EverlongPositions.sol"; /// @author DELV /// @title EverlongERC4626 @@ -9,31 +11,7 @@ import { ERC4626 } from "solady/tokens/ERC4626.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -abstract contract EverlongERC4626 is ERC4626 { - // ╭─────────────────────────────────────────────────────────╮ - // │ Storage │ - // ╰─────────────────────────────────────────────────────────╯ - - /// @notice Virtual shares are used to mitigate inflation attacks. - bool public constant useVirtualShares = true; - - /// @notice Used to reduce the feasibility of an inflation attack. - /// TODO: Determine the appropriate value for our case. Current value - /// was picked arbitrarily. - uint8 public constant decimalsOffset = 3; - - /// @dev Address of the token to use for Hyperdrive bond purchase/close. - address internal immutable _asset; - - /// @dev Decimals used by the `_asset`. - uint8 internal immutable _decimals; - - /// @dev Name of the Everlong token. - string internal _name; - - /// @dev Symbol of the Everlong token. - string internal _symbol; - +abstract contract EverlongERC4626 is ERC4626, EverlongPositions { // ╭─────────────────────────────────────────────────────────╮ // │ Constructor │ // ╰─────────────────────────────────────────────────────────╯ @@ -54,6 +32,73 @@ abstract contract EverlongERC4626 is ERC4626 { _decimals = success ? result : _DEFAULT_UNDERLYING_DECIMALS; } + // ╭─────────────────────────────────────────────────────────╮ + // │ Stateful │ + // ╰─────────────────────────────────────────────────────────╯ + + /// @inheritdoc ERC4626 + function deposit( + uint256 assets, + address to + ) public override returns (uint256 shares) { + IERC20(_asset).approve(hyperdrive, assets); + shares = super.deposit(assets, to); + } + /// @inheritdoc ERC4626 + function mint(uint256, address) public override returns (uint256 assets) { + revert("mint not implemented, please use deposit"); + } + + /// @inheritdoc ERC4626 + function withdraw( + uint256, + address, + address + ) public override returns (uint256 shares) { + revert("withdraw not implemented, please use redeem"); + } + + // ╭─────────────────────────────────────────────────────────╮ + // │ Overrides │ + // ╰─────────────────────────────────────────────────────────╯ + + // TODO: Actually estimate this based on current position contents. + // Currently, this is obtained from tracking deposits - withdrawals + // which results in Everlong taking all profit and users receiving + // exactly what they put in. + /// @notice Returns the total value of assets managed by Everlong. + /// @return Total managed asset value. + function totalAssets() public view override returns (uint256) { + return _virtualAssets; + } + + // TODO: Might not need this but including for convenience. + function _beforeWithdraw( + uint256 _assets, + uint256 + ) internal virtual override { + _virtualAssets -= _assets; + // Check if Everlong has sufficient assets to service the withdrawal. + if (IERC20(_asset).balanceOf(address(this)) < _assets) { + // Everlong does not have sufficient assets to service withdrawal. + // First try closing all mature positions. If sufficient to + // service the withdrawal, then continue. + _closeMaturedPositions(); + uint256 _currentBalance = IERC20(_asset).balanceOf(address(this)); + if (_currentBalance >= _assets) return; + + // Close remaining positions until Everlong's balance is + // enough to meet the withdrawal. + _closePositionsByOutput(_assets - _currentBalance); + } + } + + // TODO: Might not need this but including for convenience. + function _afterDeposit(uint256 _assets, uint256) internal virtual override { + _virtualAssets += _assets; + rebalance(); + } + // ╭─────────────────────────────────────────────────────────╮ // │ Internal │ // ╰─────────────────────────────────────────────────────────╯ @@ -83,16 +128,6 @@ abstract contract EverlongERC4626 is ERC4626 { return decimalsOffset; } - // TODO: Might not need this but including for convenience. - function _beforeWithdraw(uint256, uint256) internal override { - // revert("TODO"); - } - - // TODO: Might not need this but including for convenience. - function _afterDeposit(uint256, uint256) internal override { - // revert("TODO"); - } - // ╭─────────────────────────────────────────────────────────╮ // │ Getters │ // ╰─────────────────────────────────────────────────────────╯ diff --git a/contracts/internal/EverlongPositions.sol b/contracts/internal/EverlongPositions.sol index 65f3fbc..e0c402d 100644 --- a/contracts/internal/EverlongPositions.sol +++ b/contracts/internal/EverlongPositions.sol @@ -7,7 +7,7 @@ import { IERC20 } from "openzeppelin/interfaces/IERC20.sol"; import { IEverlong } from "../interfaces/IEverlong.sol"; import { IEverlongPositions } from "../interfaces/IEverlongPositions.sol"; import { Position } from "../types/Position.sol"; -import { EverlongBase } from "./EverlongBase.sol"; +import { EverlongStorage } from "./EverlongStorage.sol"; /// @author DELV /// @title EverlongPositions @@ -15,7 +15,7 @@ import { EverlongBase } from "./EverlongBase.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -abstract contract EverlongPositions is EverlongBase, IEverlongPositions { +abstract contract EverlongPositions is EverlongStorage, IEverlongPositions { using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; // ╭─────────────────────────────────────────────────────────╮ @@ -169,6 +169,33 @@ abstract contract EverlongPositions is EverlongBase, IEverlongPositions { // │ Position Closing (Internal) │ // ╰─────────────────────────────────────────────────────────╯ + /// @dev Close positions until the minimum output is met. + /// @param _minOutput Output needed from closed positions. + function _closePositionsByOutput(uint256 _minOutput) internal { + // Loop through positions and close them all. + Position memory _position; + uint256 _output; + // TODO: Enable closing of positions incrementally to avoid + // the case where the # of mature positions exceeds the max + // gas per block. + while (_output < _minOutput) { + // Retrieve the oldest position and close it. + _position = getPosition(0); + _output += IHyperdrive(hyperdrive).closeLong( + _position.maturityTime, + _position.bondAmount, + _minCloseLongOutput( + _position.maturityTime, + _position.bondAmount + ), + IHyperdrive.Options(address(this), _asBase, "") + ); + + // Update positions to reflect the newly closed long. + _handleCloseLong(uint128(_position.bondAmount)); + } + } + /// @dev Close all matured positions. function _closeMaturedPositions() internal { // Loop through mature positions and close them all. diff --git a/contracts/internal/EverlongStorage.sol b/contracts/internal/EverlongStorage.sol new file mode 100644 index 0000000..f5bf76f --- /dev/null +++ b/contracts/internal/EverlongStorage.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { DoubleEndedQueue } from "openzeppelin/utils/structs/DoubleEndedQueue.sol"; +import { IEverlongEvents } from "../interfaces/IEverlongEvents.sol"; + +// TODO: Reassess whether centralized configuration management makes sense. +// https://github.com/delvtech/everlong/pull/2#discussion_r1703799747 +/// @author DELV +/// @title EverlongStorage +/// @notice Base contract for Everlong. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +abstract contract EverlongStorage is IEverlongEvents { + using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; + + // ╭─────────────────────────────────────────────────────────╮ + // │ Storage │ + // ╰─────────────────────────────────────────────────────────╯ + + // Admin // + + /// @dev Address of the contract admin. + address internal _admin; + + // Hyperdrive // + + /// @dev Address of the Hyperdrive instance wrapped by Everlong. + address public hyperdrive; + + /// @dev Whether to use Hyperdrive's base token to purchase bonds. + // If false, use the Hyperdrive's `vaultSharesToken`. + bool internal _asBase; + + // Positions // + + // TODO: Reassess using a more tailored data structure. + /// @dev Utility data structure to manage the position queue. + /// Supports pushing and popping from both the front and back. + DoubleEndedQueue.Bytes32Deque internal _positions; + + // ERC4626 // + /// @notice Virtual shares are used to mitigate inflation attacks. + bool public constant useVirtualShares = true; + + /// @notice Used to reduce the feasibility of an inflation attack. + /// TODO: Determine the appropriate value for our case. Current value + /// was picked arbitrarily. + uint8 public constant decimalsOffset = 3; + + /// @dev Address of the token to use for Hyperdrive bond purchase/close. + address internal _asset; + + // TODO: Remove in favor of more sophisticated position valuation. + // TODO: Use some SafeMath library. + /// @dev Virtual asset count to track amount deposited into Hyperdrive. + uint256 internal _virtualAssets; + + /// @dev Decimals used by the `_asset`. + uint8 internal _decimals; + + /// @dev Name of the Everlong token. + string internal _name; + + /// @dev Symbol of the Everlong token. + string internal _symbol; +} diff --git a/test/exposed/EverlongBaseExposed.sol b/test/exposed/EverlongBaseExposed.sol new file mode 100644 index 0000000..86e1419 --- /dev/null +++ b/test/exposed/EverlongBaseExposed.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { EverlongBase } from "../../contracts/internal/EverlongBase.sol"; +import { EverlongAdminExposed } from "./EverlongAdminExposed.sol"; +import { EverlongERC4626Exposed } from "./EverlongERC4626Exposed.sol"; + +/// @title EverlongExposed +/// @dev Exposes all internal functions for the `Everlong` contract. +abstract contract EverlongBaseExposed is + EverlongAdminExposed, + EverlongERC4626Exposed, + EverlongBase +{} diff --git a/test/exposed/EverlongERC4626Exposed.sol b/test/exposed/EverlongERC4626Exposed.sol new file mode 100644 index 0000000..48102b1 --- /dev/null +++ b/test/exposed/EverlongERC4626Exposed.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +import { EverlongPositionsExposed } from "../exposed/EverlongPositionsExposed.sol"; +import { EverlongERC4626 } from "../../contracts/internal/EverlongERC4626.sol"; + +/// @title EverlongERC4626Exposed +/// @dev Exposes all internal functions for the `EverlongERC4626` contract. +abstract contract EverlongERC4626Exposed is + EverlongERC4626, + EverlongPositionsExposed +{ + 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/EverlongExposed.sol b/test/exposed/EverlongExposed.sol index 15c6a85..2333e93 100644 --- a/test/exposed/EverlongExposed.sol +++ b/test/exposed/EverlongExposed.sol @@ -2,17 +2,12 @@ pragma solidity 0.8.20; import { Test } from "forge-std/Test.sol"; -import { EverlongBase } from "../../contracts/internal/EverlongBase.sol"; -import { EverlongAdminExposed } from "./EverlongAdminExposed.sol"; -import { EverlongPositionsExposed } from "./EverlongPositionsExposed.sol"; +import { Everlong } from "../../contracts/Everlong.sol"; +import { EverlongBaseExposed } from "./EverlongBaseExposed.sol"; /// @title EverlongExposed /// @dev Exposes all internal functions for the `Everlong` contract. -contract EverlongExposed is - EverlongAdminExposed, - EverlongPositionsExposed, - Test -{ +contract EverlongExposed is EverlongBaseExposed, Everlong, Test { /// @notice Initial configuration paramters for Everlong. /// @param hyperdrive_ Address of the Hyperdrive instance wrapped by Everlong. /// @param name_ Name of the ERC20 token managed by Everlong. @@ -23,5 +18,5 @@ contract EverlongExposed is string memory symbol_, address hyperdrive_, bool asBase_ - ) EverlongBase(name_, symbol_, hyperdrive_, asBase_) {} + ) Everlong(name_, symbol_, hyperdrive_, asBase_) {} } diff --git a/test/harnesses/EverlongTest.sol b/test/harnesses/EverlongTest.sol index 4b523b5..5f3ac18 100644 --- a/test/harnesses/EverlongTest.sol +++ b/test/harnesses/EverlongTest.sol @@ -84,14 +84,12 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { uint256 amount ) internal { ERC20Mintable(hyperdrive.baseToken()).mint(recipient, amount); + vm.startPrank(recipient); ERC20Mintable(hyperdrive.baseToken()).approve( address(everlong), - type(uint256).max - ); - ERC20Mintable(hyperdrive.baseToken()).approve( - address(hyperdrive), - type(uint256).max + amount ); + vm.stopPrank(); } // TODO: This is gross, will refactor diff --git a/test/units/EverlongERC4626.t.sol b/test/units/EverlongERC4626.t.sol new file mode 100644 index 0000000..1c78516 --- /dev/null +++ b/test/units/EverlongERC4626.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.20; + +// solhint-disable-next-line no-console, no-unused-import +import { console2 as console } from "forge-std/console2.sol"; +import { ERC20Mintable } from "hyperdrive/contracts/test/ERC20Mintable.sol"; +import { EverlongTest } from "../harnesses/EverlongTest.sol"; +import { IEverlongEvents } from "../../contracts/interfaces/IEverlongEvents.sol"; + +/// @dev Tests EverlongERC4626 functionality. +contract TestEverlongERC4626 is EverlongTest { + // ╭─────────────────────────────────────────────────────────╮ + // │ Deposit/Mint │ + // ╰─────────────────────────────────────────────────────────╯ + + /// @dev Validates that `deposit()` rebalances positions. + function test_deposit_causes_rebalance() external { + // Initialize the Hyperdrive instance by adding liquidity from Alice. + uint256 fixedRate = 0.5e18; + uint256 contribution = 10_000e18; + initialize(alice, fixedRate, contribution); + + // Mint bob some assets and approve the Everlong contract. + mintApproveHyperdriveBase(bob, 1e18); + + // Deposit assets into Everlong as Bob. + vm.startPrank(bob); + vm.expectEmit(true, true, true, true); + emit IEverlongEvents.Rebalanced(); + everlong.deposit(1e18, bob); + vm.stopPrank(); + } + + /// @dev Validates that `_afterDeposit` increases total assets. + function test_afterDeposit_virtual_asset_increase() external { + // Call `_afterDeposit` with some assets. + everlong.exposed_afterDeposit(5, 1); + // Ensure `totalAssets()` is increased by the correct amount. + assertEq(everlong.totalAssets(), 5); + } + + // ╭─────────────────────────────────────────────────────────╮ + // │ Withdraw/Redeem │ + // ╰─────────────────────────────────────────────────────────╯ + + /// @dev Validates that `redeem()` will close all positions with a + // mature position sufficient to cover withdrawal amount. + function test_redeem_close_positions_mature() external { + // Initialize the Hyperdrive instance by adding liquidity from Alice. + uint256 fixedRate = 0.5e18; + uint256 contribution = 10_000e18; + initialize(alice, fixedRate, contribution); + + // Mint Bob and Celine some assets and approve the Everlong contract. + mintApproveHyperdriveBase(bob, 10e18); + mintApproveHyperdriveBase(celine, 2e18); + + // Deposit assets into Everlong as Bob. + vm.startPrank(bob); + everlong.deposit(1e18, bob); + vm.stopPrank(); + + // Advance time by a checkpoint so that future deposits result in + // new positions. + advanceTime(hyperdrive.getPoolConfig().checkpointDuration, 0); + + // Confirm that Everlong currently does not have a matured position. + assertFalse(everlong.hasMaturedPositions()); + + // Deposit assets into Everlong as Celine to create another position. + vm.startPrank(celine); + everlong.deposit(2e18, celine); + vm.stopPrank(); + + // Confirm that Everlong now has two positions. + assertEq(everlong.getPositionCount(), 2); + + // Advance time to mature Bob's position but not Celine's. + advanceTime(everlong.getPosition(0).maturityTime - block.timestamp, 0); + + // Confirm that Everlong now has a matured position. + assertTrue(everlong.hasMaturedPositions()); + + // Redeem all of Bob's shares. + vm.startPrank(bob); + everlong.redeem(everlong.balanceOf(bob), bob, bob); + vm.stopPrank(); + + // Confirm that Everlong now has one immature position. + assertEq(everlong.getPositionCount(), 1); + assertGt(everlong.getPosition(0).maturityTime, block.timestamp); + } + + /// @dev Validates that `redeem()` will close all positions when closing + /// an immature position is required to service the withdrawal. + function test_redeem_close_all_positions_immature() external { + // Initialize the Hyperdrive instance by adding liquidity from Alice. + uint256 fixedRate = 0.5e18; + uint256 contribution = 10_000e18; + initialize(alice, fixedRate, contribution); + + // Mint Bob and Celine some assets and approve the Everlong contract. + mintApproveHyperdriveBase(bob, 10e18); + + // Deposit assets into Everlong as Bob. + vm.startPrank(bob); + everlong.deposit(1e18, bob); + vm.stopPrank(); + + // Advance time by a checkpoint so that future deposits result in + // new positions. + advanceTime(hyperdrive.getPoolConfig().checkpointDuration, 0); + + // Confirm that Everlong currently does not have a matured position. + assertFalse(everlong.hasMaturedPositions()); + + // Deposit more assets into Everlong as Bob to create another position. + vm.startPrank(bob); + everlong.deposit(2e18, bob); + vm.stopPrank(); + + // Confirm that Everlong now has two positions. + assertEq(everlong.getPositionCount(), 2); + + // Advance time to mature only the first position. + advanceTime(everlong.getPosition(0).maturityTime - block.timestamp, 0); + + // Confirm that Everlong now has a matured position. + assertTrue(everlong.hasMaturedPositions()); + + // Redeem all of Bob's shares. + vm.startPrank(bob); + everlong.redeem(everlong.balanceOf(bob), bob, bob); + vm.stopPrank(); + } + + /// @dev Validates that `_beforeWithdraw` decreases total assets. + function test_beforeWithdraw_virtual_asset_decrease() external { + // Call `_afterDeposit` to increase total asset count. + everlong.exposed_afterDeposit(5, 1); + + // Mint Everlong some assets so it can pass the withdrawal + // balance check. + ERC20Mintable(everlong.asset()).mint(address(everlong), 5); + + // Call `_beforeWithdraw` to decrease total asset count. + everlong.exposed_beforeWithdraw(5, 1); + + // Ensure `totalAssets()` is decreased by the correct amount. + assertEq(everlong.totalAssets(), 0); + } +} From d086ea7a69eb261e05d6df51760d681a55c91059 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Wed, 7 Aug 2024 05:19:05 -0500 Subject: [PATCH 02/27] reformat second-level comments --- contracts/interfaces/IEverlong.sol | 4 ++-- contracts/interfaces/IEverlongEvents.sol | 4 ++-- contracts/internal/EverlongStorage.sol | 9 +++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/contracts/interfaces/IEverlong.sol b/contracts/interfaces/IEverlong.sol index fd00542..c1accbc 100644 --- a/contracts/interfaces/IEverlong.sol +++ b/contracts/interfaces/IEverlong.sol @@ -31,12 +31,12 @@ interface IEverlong is // │ Errors │ // ╰─────────────────────────────────────────────────────────╯ - // Admin // + // ── Admin ────────────────────────────────────────────────── /// @notice Thrown when caller is not the admin. error Unauthorized(); - // Positions // + // ── Positions ────────────────────────────────────────────── /// @notice Thrown when attempting to insert a position with /// a `maturityTime` sooner than the most recent position's. diff --git a/contracts/interfaces/IEverlongEvents.sol b/contracts/interfaces/IEverlongEvents.sol index 753a977..5ced7a3 100644 --- a/contracts/interfaces/IEverlongEvents.sol +++ b/contracts/interfaces/IEverlongEvents.sol @@ -2,12 +2,12 @@ pragma solidity 0.8.20; interface IEverlongEvents { - // Admin // + // ── Admin ────────────────────────────────────────────────── /// @notice Emitted when admin is transferred. event AdminUpdated(address indexed admin); - // Positions // + // ── Positions ────────────────────────────────────────────── /// @notice Emitted when a new position is added to the bond portfolio. /// @dev This event will only be emitted with new `maturityTime`s in the portfolio. diff --git a/contracts/internal/EverlongStorage.sol b/contracts/internal/EverlongStorage.sol index f5bf76f..f242cf2 100644 --- a/contracts/internal/EverlongStorage.sol +++ b/contracts/internal/EverlongStorage.sol @@ -19,12 +19,12 @@ abstract contract EverlongStorage is IEverlongEvents { // │ Storage │ // ╰─────────────────────────────────────────────────────────╯ - // Admin // + // ── Admin ────────────────────────────────────────────────── /// @dev Address of the contract admin. address internal _admin; - // Hyperdrive // + // ── Hyperdrive ───────────────────────────────────────────── /// @dev Address of the Hyperdrive instance wrapped by Everlong. address public hyperdrive; @@ -33,14 +33,15 @@ abstract contract EverlongStorage is IEverlongEvents { // If false, use the Hyperdrive's `vaultSharesToken`. bool internal _asBase; - // Positions // + // ── Positions ────────────────────────────────────────────── // TODO: Reassess using a more tailored data structure. /// @dev Utility data structure to manage the position queue. /// Supports pushing and popping from both the front and back. DoubleEndedQueue.Bytes32Deque internal _positions; - // ERC4626 // + // ── ERC4626 ──────────────────────────────────────────────── + /// @notice Virtual shares are used to mitigate inflation attacks. bool public constant useVirtualShares = true; From 6b3d5b2162a0f214b192172650764c4e258f2bc8 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Wed, 7 Aug 2024 13:39:09 -0500 Subject: [PATCH 03/27] redeem override + comment updates + contribution guide --- CONTRIBUTING.md | 12 ++++ contracts/internal/EverlongERC4626.sol | 13 ++++ contracts/internal/EverlongPositions.sol | 26 +++---- test/exposed/EverlongBaseExposed.sol | 4 +- test/units/EverlongERC4626.t.sol | 86 ++++++++++++++++-------- 5 files changed, 97 insertions(+), 44 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..bb5736c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# Contributing to Everlong + +## Bugfixes & issues + +If you find a bug, we recommend that you post an [issue](https://github.com/delvtech/everlong/issues) to allow for discussion before creating a pull request. + +## Release steps + +1. [ ] Double-check that the version in `contracts/libraries/Constants.sol` matches the version to be tagged. If it doesn't, open a PR to update the version before tagging. +2. [ ] Tag the release with `git tag vX.Y.Z`. +3. [ ] Push the release to Github with `git push --tags` +4. [ ] Go to the `releases` tab in Github and add the new tag as a release. Click the "Generate Release Notes" button to generate release notes. diff --git a/contracts/internal/EverlongERC4626.sol b/contracts/internal/EverlongERC4626.sol index d8466c0..73964da 100644 --- a/contracts/internal/EverlongERC4626.sol +++ b/contracts/internal/EverlongERC4626.sol @@ -44,11 +44,24 @@ abstract contract EverlongERC4626 is ERC4626, EverlongPositions { IERC20(_asset).approve(hyperdrive, assets); shares = super.deposit(assets, to); } + + /// @inheritdoc ERC4626 + function redeem( + uint256 shares, + address to, + address owner + ) public override returns (uint256 assets) { + assets = super.redeem(shares, to, owner); + rebalance(); + } + + // TODO: Implement. /// @inheritdoc ERC4626 function mint(uint256, address) public override returns (uint256 assets) { revert("mint not implemented, please use deposit"); } + // TODO: Implement. /// @inheritdoc ERC4626 function withdraw( uint256, diff --git a/contracts/internal/EverlongPositions.sol b/contracts/internal/EverlongPositions.sol index e0c402d..e92b8d1 100644 --- a/contracts/internal/EverlongPositions.sol +++ b/contracts/internal/EverlongPositions.sol @@ -122,21 +122,20 @@ abstract contract EverlongPositions is EverlongStorage, IEverlongPositions { uint128 _maturityTime, uint128 _bondAmountPurchased ) internal { - // Compare the maturity time of the purchased bonds - // to the most recent position's `maturityTime`. + // Revert if the incoming position's `maturityTime` + // is sooner than the most recently added position's maturity. if ( _positions.length() != 0 && _decodePosition(_positions.back()).maturityTime > _maturityTime ) { - // Revert because the incoming position's `maturityTime` - // is sooner than the most recently added position's maturity. revert IEverlong.InconsistentPositionMaturity(); - } else if ( + } + // A position already exists with the incoming `maturityTime`. + // The existing position's `bondAmount` is updated. + else if ( _positions.length() != 0 && _decodePosition(_positions.back()).maturityTime == _maturityTime ) { - // A position already exists with the incoming `maturityTime`. - // The existing position's `bondAmount` is updated. Position memory _oldPosition = _decodePosition( _positions.popBack() ); @@ -151,9 +150,10 @@ abstract contract EverlongPositions is EverlongStorage, IEverlongPositions { _oldPosition.bondAmount + _bondAmountPurchased, _positions.length() - 1 ); - } else { - // No position exists with the incoming `maturityTime`. - // Push a new position to the end of the queue. + } + // No position exists with the incoming `maturityTime`. + // Push a new position to the end of the queue. + else { _positions.pushBack( _encodePosition(_maturityTime, _bondAmountPurchased) ); @@ -173,11 +173,11 @@ abstract contract EverlongPositions is EverlongStorage, IEverlongPositions { /// @param _minOutput Output needed from closed positions. function _closePositionsByOutput(uint256 _minOutput) internal { // Loop through positions and close them all. - Position memory _position; - uint256 _output; // TODO: Enable closing of positions incrementally to avoid // the case where the # of mature positions exceeds the max // gas per block. + Position memory _position; + uint256 _output; while (_output < _minOutput) { // Retrieve the oldest position and close it. _position = getPosition(0); @@ -199,10 +199,10 @@ abstract contract EverlongPositions is EverlongStorage, IEverlongPositions { /// @dev Close all matured positions. function _closeMaturedPositions() internal { // Loop through mature positions and close them all. - Position memory _position; // TODO: Enable closing of mature positions incrementally to avoid // the case where the # of mature positions exceeds the max // gas per block. + Position memory _position; while (hasMaturedPositions()) { // Retrieve the oldest matured position and close it. _position = getPosition(0); diff --git a/test/exposed/EverlongBaseExposed.sol b/test/exposed/EverlongBaseExposed.sol index 86e1419..0d0c239 100644 --- a/test/exposed/EverlongBaseExposed.sol +++ b/test/exposed/EverlongBaseExposed.sol @@ -5,8 +5,8 @@ import { EverlongBase } from "../../contracts/internal/EverlongBase.sol"; import { EverlongAdminExposed } from "./EverlongAdminExposed.sol"; import { EverlongERC4626Exposed } from "./EverlongERC4626Exposed.sol"; -/// @title EverlongExposed -/// @dev Exposes all internal functions for the `Everlong` contract. +/// @title EverlongBaseExposed +/// @dev Exposes all internal functions for the `EverlongBase` contract. abstract contract EverlongBaseExposed is EverlongAdminExposed, EverlongERC4626Exposed, diff --git a/test/units/EverlongERC4626.t.sol b/test/units/EverlongERC4626.t.sol index 1c78516..5fabcfe 100644 --- a/test/units/EverlongERC4626.t.sol +++ b/test/units/EverlongERC4626.t.sol @@ -9,53 +9,66 @@ import { IEverlongEvents } from "../../contracts/interfaces/IEverlongEvents.sol" /// @dev Tests EverlongERC4626 functionality. contract TestEverlongERC4626 is EverlongTest { - // ╭─────────────────────────────────────────────────────────╮ - // │ Deposit/Mint │ - // ╰─────────────────────────────────────────────────────────╯ - - /// @dev Validates that `deposit()` rebalances positions. - function test_deposit_causes_rebalance() external { + /// @dev Validates behavior of the `deposit()` function for a single + /// depositor. + /// @dev When calling the `deposit()` function... + /// 1. The `Rebalanced` event should be emitted + /// 2. The balance of the Everlong contract should be minimal. + /// 3. The share amount issued to the depositor should be equal to the + /// total supply of Everlong shares. + /// 4. The amount of assets deposited should relate to the share + /// count as follows: + /// shares_issued = assets_deposited * (10 ^ decimalsOffset) + function test_deposit_single_depositor() external { // Initialize the Hyperdrive instance by adding liquidity from Alice. uint256 fixedRate = 0.5e18; uint256 contribution = 10_000e18; initialize(alice, fixedRate, contribution); // Mint bob some assets and approve the Everlong contract. - mintApproveHyperdriveBase(bob, 1e18); + uint256 depositAmount = 1e18; + mintApproveHyperdriveBase(bob, depositAmount); - // Deposit assets into Everlong as Bob. + // 1. Deposit assets into Everlong as Bob and confirm `Rebalanced` event + // is emitted. vm.startPrank(bob); vm.expectEmit(true, true, true, true); emit IEverlongEvents.Rebalanced(); - everlong.deposit(1e18, bob); + everlong.deposit(depositAmount, bob); vm.stopPrank(); - } - /// @dev Validates that `_afterDeposit` increases total assets. - function test_afterDeposit_virtual_asset_increase() external { - // Call `_afterDeposit` with some assets. - everlong.exposed_afterDeposit(5, 1); - // Ensure `totalAssets()` is increased by the correct amount. - assertEq(everlong.totalAssets(), 5); + // 2. Confirm that Everlong's balance is less than Hyperdrive's + // minimum transaction amount. + assertLt( + ERC20Mintable(everlong.asset()).balanceOf(address(everlong)), + hyperdrive.getPoolConfig().minimumTransactionAmount, + "Everlong balance should be below min tx amount after single depositor deposit+rebalance" + ); + + // 3. Confirm that Bob's share balance equals the total supply of shares. + assertEq( + everlong.balanceOf(bob), + everlong.totalSupply(), + "single depositor share balance should equal total supply of shares" + ); + + // 4. Confirm `shares_issued = assets_deposited * (10 ^ decimalsOffset)` + assertEq( + everlong.balanceOf(bob), + depositAmount * (10 ** everlong.decimalsOffset()) + ); } - // ╭─────────────────────────────────────────────────────────╮ - // │ Withdraw/Redeem │ - // ╰─────────────────────────────────────────────────────────╯ - - /// @dev Validates that `redeem()` will close all positions with a - // mature position sufficient to cover withdrawal amount. + /// @dev Validates that `redeem()` will close the necessary positions with a + /// mature position sufficient to cover withdrawal amount. function test_redeem_close_positions_mature() external { // Initialize the Hyperdrive instance by adding liquidity from Alice. uint256 fixedRate = 0.5e18; uint256 contribution = 10_000e18; initialize(alice, fixedRate, contribution); - // Mint Bob and Celine some assets and approve the Everlong contract. - mintApproveHyperdriveBase(bob, 10e18); - mintApproveHyperdriveBase(celine, 2e18); - // Deposit assets into Everlong as Bob. + mintApproveHyperdriveBase(bob, 10e18); vm.startPrank(bob); everlong.deposit(1e18, bob); vm.stopPrank(); @@ -68,6 +81,7 @@ contract TestEverlongERC4626 is EverlongTest { assertFalse(everlong.hasMaturedPositions()); // Deposit assets into Everlong as Celine to create another position. + mintApproveHyperdriveBase(celine, 2e18); vm.startPrank(celine); everlong.deposit(2e18, celine); vm.stopPrank(); @@ -86,14 +100,16 @@ contract TestEverlongERC4626 is EverlongTest { everlong.redeem(everlong.balanceOf(bob), bob, bob); vm.stopPrank(); - // Confirm that Everlong now has one immature position. - assertEq(everlong.getPositionCount(), 1); + // Confirm that Everlong has two immature positions: + // 1. Created from Celine's deposit. + // 2. Created from rebalance on Bob's redemption. + assertEq(everlong.getPositionCount(), 2); assertGt(everlong.getPosition(0).maturityTime, block.timestamp); } /// @dev Validates that `redeem()` will close all positions when closing /// an immature position is required to service the withdrawal. - function test_redeem_close_all_positions_immature() external { + function test_redeem_close_positions_immature() external { // Initialize the Hyperdrive instance by adding liquidity from Alice. uint256 fixedRate = 0.5e18; uint256 contribution = 10_000e18; @@ -132,6 +148,18 @@ contract TestEverlongERC4626 is EverlongTest { vm.startPrank(bob); everlong.redeem(everlong.balanceOf(bob), bob, bob); vm.stopPrank(); + + // Confirm that Everlong now has only the position created from + // rebalancing on Bob's redemption. + assertEq(everlong.getPositionCount(), 1); + } + + /// @dev Validates that `_afterDeposit` increases total assets. + function test_afterDeposit_virtual_asset_increase() external { + // Call `_afterDeposit` with some assets. + everlong.exposed_afterDeposit(5, 1); + // Ensure `totalAssets()` is increased by the correct amount. + assertEq(everlong.totalAssets(), 5); } /// @dev Validates that `_beforeWithdraw` decreases total assets. From 07c4105773b1a074e03c8027264692dd6c45db84 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Wed, 7 Aug 2024 13:51:02 -0500 Subject: [PATCH 04/27] update contract version to 0.8.22 and relax lib/interface/test versions --- contracts/Everlong.sol | 2 +- contracts/interfaces/IEverlong.sol | 2 +- contracts/interfaces/IEverlongAdmin.sol | 2 +- contracts/interfaces/IEverlongEvents.sol | 2 +- contracts/interfaces/IEverlongPositions.sol | 2 +- contracts/internal/EverlongAdmin.sol | 2 +- contracts/internal/EverlongBase.sol | 2 +- contracts/internal/EverlongERC4626.sol | 2 +- contracts/internal/EverlongPositions.sol | 2 +- contracts/internal/EverlongStorage.sol | 2 +- contracts/libraries/Constants.sol | 2 +- contracts/types/Position.sol | 2 +- foundry.toml | 2 +- lib/hyperdrive | 2 +- test/exposed/EverlongAdminExposed.sol | 2 +- test/exposed/EverlongBaseExposed.sol | 2 +- test/exposed/EverlongERC4626Exposed.sol | 2 +- test/exposed/EverlongExposed.sol | 2 +- test/exposed/EverlongPositionsExposed.sol | 2 +- test/harnesses/EverlongTest.sol | 2 +- test/units/EverlongAdmin.t.sol | 2 +- test/units/EverlongERC4626.t.sol | 2 +- test/units/EverlongPositions.t.sol | 2 +- test/units/IEverlong.t.sol | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index 4364356..ad92cff 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity 0.8.22; import { EverlongBase } from "./internal/EverlongBase.sol"; diff --git a/contracts/interfaces/IEverlong.sol b/contracts/interfaces/IEverlong.sol index c1accbc..6d9efbf 100644 --- a/contracts/interfaces/IEverlong.sol +++ b/contracts/interfaces/IEverlong.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; import { IERC4626 } from "openzeppelin/interfaces/IERC4626.sol"; import { IEverlongAdmin } from "./IEverlongAdmin.sol"; diff --git a/contracts/interfaces/IEverlongAdmin.sol b/contracts/interfaces/IEverlongAdmin.sol index f957411..e54c226 100644 --- a/contracts/interfaces/IEverlongAdmin.sol +++ b/contracts/interfaces/IEverlongAdmin.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; interface IEverlongAdmin { // ╭─────────────────────────────────────────────────────────╮ diff --git a/contracts/interfaces/IEverlongEvents.sol b/contracts/interfaces/IEverlongEvents.sol index 5ced7a3..983638d 100644 --- a/contracts/interfaces/IEverlongEvents.sol +++ b/contracts/interfaces/IEverlongEvents.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; interface IEverlongEvents { // ── Admin ────────────────────────────────────────────────── diff --git a/contracts/interfaces/IEverlongPositions.sol b/contracts/interfaces/IEverlongPositions.sol index dcc1ace..6759fbf 100644 --- a/contracts/interfaces/IEverlongPositions.sol +++ b/contracts/interfaces/IEverlongPositions.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; import { Position } from "../types/Position.sol"; diff --git a/contracts/internal/EverlongAdmin.sol b/contracts/internal/EverlongAdmin.sol index f5e12a0..60a82d0 100644 --- a/contracts/internal/EverlongAdmin.sol +++ b/contracts/internal/EverlongAdmin.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity 0.8.22; import { IEverlong } from "../interfaces/IEverlong.sol"; import { IEverlongAdmin } from "../interfaces/IEverlongAdmin.sol"; diff --git a/contracts/internal/EverlongBase.sol b/contracts/internal/EverlongBase.sol index 418728d..fcc542a 100644 --- a/contracts/internal/EverlongBase.sol +++ b/contracts/internal/EverlongBase.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity 0.8.22; import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; import { DoubleEndedQueue } from "openzeppelin/utils/structs/DoubleEndedQueue.sol"; diff --git a/contracts/internal/EverlongERC4626.sol b/contracts/internal/EverlongERC4626.sol index 73964da..10b157b 100644 --- a/contracts/internal/EverlongERC4626.sol +++ b/contracts/internal/EverlongERC4626.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity 0.8.22; import { IERC20 } from "openzeppelin/interfaces/IERC20.sol"; import { ERC4626 } from "solady/tokens/ERC4626.sol"; diff --git a/contracts/internal/EverlongPositions.sol b/contracts/internal/EverlongPositions.sol index e92b8d1..dfd98c3 100644 --- a/contracts/internal/EverlongPositions.sol +++ b/contracts/internal/EverlongPositions.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity 0.8.22; import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; import { DoubleEndedQueue } from "openzeppelin/utils/structs/DoubleEndedQueue.sol"; diff --git a/contracts/internal/EverlongStorage.sol b/contracts/internal/EverlongStorage.sol index f242cf2..9ce3950 100644 --- a/contracts/internal/EverlongStorage.sol +++ b/contracts/internal/EverlongStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity 0.8.22; import { DoubleEndedQueue } from "openzeppelin/utils/structs/DoubleEndedQueue.sol"; import { IEverlongEvents } from "../interfaces/IEverlongEvents.sol"; diff --git a/contracts/libraries/Constants.sol b/contracts/libraries/Constants.sol index 51f84ae..23b41e9 100644 --- a/contracts/libraries/Constants.sol +++ b/contracts/libraries/Constants.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; string constant EVERLONG_VERSION = "v0.0.1"; diff --git a/contracts/types/Position.sol b/contracts/types/Position.sol index f15946d..f048202 100644 --- a/contracts/types/Position.sol +++ b/contracts/types/Position.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; /// @notice Tracks the total amount of bonds managed by Everlong /// with the same maturityTime. diff --git a/foundry.toml b/foundry.toml index f10a6bc..c8f1e0d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,5 +1,5 @@ [profile.default] -solc_version = '0.8.20' +solc_version = '0.8.22' # The source directory src = "contracts" # The artifact directory diff --git a/lib/hyperdrive b/lib/hyperdrive index 7beec95..9606421 160000 --- a/lib/hyperdrive +++ b/lib/hyperdrive @@ -1 +1 @@ -Subproject commit 7beec957cdde4a769b37cc1a71de3eedec561f8c +Subproject commit 960642176c9abaf948250bb186b4a57f4634e8ec diff --git a/test/exposed/EverlongAdminExposed.sol b/test/exposed/EverlongAdminExposed.sol index 048ad5d..0be9790 100644 --- a/test/exposed/EverlongAdminExposed.sol +++ b/test/exposed/EverlongAdminExposed.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; import { EverlongAdmin } from "../../contracts/internal/EverlongAdmin.sol"; diff --git a/test/exposed/EverlongBaseExposed.sol b/test/exposed/EverlongBaseExposed.sol index 0d0c239..4e2ddb8 100644 --- a/test/exposed/EverlongBaseExposed.sol +++ b/test/exposed/EverlongBaseExposed.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; import { EverlongBase } from "../../contracts/internal/EverlongBase.sol"; import { EverlongAdminExposed } from "./EverlongAdminExposed.sol"; diff --git a/test/exposed/EverlongERC4626Exposed.sol b/test/exposed/EverlongERC4626Exposed.sol index 48102b1..7195ad4 100644 --- a/test/exposed/EverlongERC4626Exposed.sol +++ b/test/exposed/EverlongERC4626Exposed.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; import { EverlongPositionsExposed } from "../exposed/EverlongPositionsExposed.sol"; import { EverlongERC4626 } from "../../contracts/internal/EverlongERC4626.sol"; diff --git a/test/exposed/EverlongExposed.sol b/test/exposed/EverlongExposed.sol index 2333e93..a9f51fd 100644 --- a/test/exposed/EverlongExposed.sol +++ b/test/exposed/EverlongExposed.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; import { Test } from "forge-std/Test.sol"; import { Everlong } from "../../contracts/Everlong.sol"; diff --git a/test/exposed/EverlongPositionsExposed.sol b/test/exposed/EverlongPositionsExposed.sol index 6eb9679..e2131fe 100644 --- a/test/exposed/EverlongPositionsExposed.sol +++ b/test/exposed/EverlongPositionsExposed.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; import { EverlongPositions } from "../../contracts/internal/EverlongPositions.sol"; diff --git a/test/harnesses/EverlongTest.sol b/test/harnesses/EverlongTest.sol index 5f3ac18..83d9831 100644 --- a/test/harnesses/EverlongTest.sol +++ b/test/harnesses/EverlongTest.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; // solhint-disable-next-line no-console, no-unused-import import { console2 as console } from "forge-std/console2.sol"; diff --git a/test/units/EverlongAdmin.t.sol b/test/units/EverlongAdmin.t.sol index ebde77f..357b2f7 100644 --- a/test/units/EverlongAdmin.t.sol +++ b/test/units/EverlongAdmin.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; // solhint-disable-next-line no-console, no-unused-import import { console2 as console } from "forge-std/console2.sol"; diff --git a/test/units/EverlongERC4626.t.sol b/test/units/EverlongERC4626.t.sol index 5fabcfe..abe787d 100644 --- a/test/units/EverlongERC4626.t.sol +++ b/test/units/EverlongERC4626.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; // solhint-disable-next-line no-console, no-unused-import import { console2 as console } from "forge-std/console2.sol"; diff --git a/test/units/EverlongPositions.t.sol b/test/units/EverlongPositions.t.sol index f2049d7..5eb0421 100644 --- a/test/units/EverlongPositions.t.sol +++ b/test/units/EverlongPositions.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; // solhint-disable-next-line no-console, no-unused-import import { console2 as console } from "forge-std/console2.sol"; diff --git a/test/units/IEverlong.t.sol b/test/units/IEverlong.t.sol index d904f2f..256715e 100644 --- a/test/units/IEverlong.t.sol +++ b/test/units/IEverlong.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.20; +pragma solidity ^0.8.20; import { IERC20 } from "openzeppelin/interfaces/IERC20.sol"; import { EverlongTest } from "../harnesses/EverlongTest.sol"; From dcb7b434434a0e05907522e4b4708986547a1afe Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Wed, 7 Aug 2024 14:15:47 -0500 Subject: [PATCH 05/27] remove redundant comment --- contracts/internal/EverlongStorage.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/contracts/internal/EverlongStorage.sol b/contracts/internal/EverlongStorage.sol index 9ce3950..3b38454 100644 --- a/contracts/internal/EverlongStorage.sol +++ b/contracts/internal/EverlongStorage.sol @@ -14,11 +14,6 @@ import { IEverlongEvents } from "../interfaces/IEverlongEvents.sol"; /// particular legal or regulatory significance. abstract contract EverlongStorage is IEverlongEvents { using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; - - // ╭─────────────────────────────────────────────────────────╮ - // │ Storage │ - // ╰─────────────────────────────────────────────────────────╯ - // ── Admin ────────────────────────────────────────────────── /// @dev Address of the contract admin. From 207a691897532bfe1a6c5daa503b8e9f4b7da185 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Wed, 7 Aug 2024 14:20:47 -0500 Subject: [PATCH 06/27] cleanup Everlong unit test --- test/units/Everlong.t.sol | 26 +++++ test/units/EverlongERC4626.t.sol | 32 ++++++ test/units/EverlongPositions.t.sol | 124 ++++++++++++++++++++- test/units/IEverlong.t.sol | 169 ----------------------------- 4 files changed, 181 insertions(+), 170 deletions(-) create mode 100644 test/units/Everlong.t.sol delete mode 100644 test/units/IEverlong.t.sol diff --git a/test/units/Everlong.t.sol b/test/units/Everlong.t.sol new file mode 100644 index 0000000..610e5b8 --- /dev/null +++ b/test/units/Everlong.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import { EverlongTest } from "../harnesses/EverlongTest.sol"; + +/// @dev +contract TestEverlong is EverlongTest { + /// @dev Ensure that the `hyperdrive()` view function is implemented. + function test_hyperdrive() external view { + assertEq( + everlong.hyperdrive(), + address(hyperdrive), + "hyperdrive() should return hyperdrive address" + ); + } + + /// @dev Ensure that the `kind()` view function is implemented. + function test_kind() external view { + assertNotEq(everlong.kind(), "", "kind is empty string"); + } + + /// @dev Ensure that the `version()` view function is implemented. + function test_version() external view { + assertNotEq(everlong.version(), "", "version is empty string"); + } +} diff --git a/test/units/EverlongERC4626.t.sol b/test/units/EverlongERC4626.t.sol index abe787d..fecdaec 100644 --- a/test/units/EverlongERC4626.t.sol +++ b/test/units/EverlongERC4626.t.sol @@ -154,6 +154,38 @@ contract TestEverlongERC4626 is EverlongTest { assertEq(everlong.getPositionCount(), 1); } + /// @dev Ensure that the `hyperdrive()` view function is implemented. + function test_hyperdrive() external view { + assertEq( + everlong.hyperdrive(), + address(hyperdrive), + "hyperdrive() should return hyperdrive address" + ); + } + + /// @dev Ensure that the `asset()` view function is implemented. + function test_asset() external view { + assertEq( + everlong.asset(), + address(hyperdrive.baseToken()), + "asset() should return hyperdrive base token address" + ); + } + + /// @dev Ensure that the `name()` view function is implemented. + function test_name() external view { + assertNotEq(everlong.name(), "", "name() not return an empty string"); + } + + /// @dev Ensure that the `symbol()` view function is implemented. + function test_symbol() external view { + assertNotEq( + everlong.symbol(), + "", + "symbol() not return an empty string" + ); + } + /// @dev Validates that `_afterDeposit` increases total assets. function test_afterDeposit_virtual_asset_increase() external { // Call `_afterDeposit` with some assets. diff --git a/test/units/EverlongPositions.t.sol b/test/units/EverlongPositions.t.sol index 5eb0421..b947a6e 100644 --- a/test/units/EverlongPositions.t.sol +++ b/test/units/EverlongPositions.t.sol @@ -62,7 +62,10 @@ contract TestEverlongPositions is EverlongTest { /// @dev Validate that `hasSufficientExcessLiquidity` returns false /// when Everlong has no balance. - function test_hasSufficientExcessLiquidity_false_no_balance() external { + function test_hasSufficientExcessLiquidity_false_no_balance() + external + view + { // Check that the contract has no balance. assertEq(IERC20(everlong.asset()).balanceOf(address(everlong)), 0); // Check that `hasSufficientExcessLiquidity` returns false. @@ -226,4 +229,123 @@ contract TestEverlongPositions is EverlongTest { "position count should be 1 after opening and closing a long for the partial bond amount" ); } + + /// @dev Ensure that `canRebalance()` returns false when everlong has + /// no positions nor balance. + function test_canRebalance_false_no_positions_no_balance() external view { + // Check that Everlong: + // - has no positions + // - has no balance + // - `canRebalance()` returns false + assertEq( + everlong.getPositionCount(), + 0, + "everlong should not intialize with positions" + ); + assertEq( + IERC20(everlong.asset()).balanceOf(address(everlong)), + 0, + "everlong should not initialize with a balance" + ); + assertFalse( + everlong.canRebalance(), + "cannot rebalance without matured positions or balance" + ); + } + + /// @dev Ensures that `canRebalance()` only returns true with a balance + /// greater than or equal to Hyperdrive's minTransactionAmount or + /// with matured positions. + /// @dev The test goes through the following steps: + /// 1. Mint `asset` to the Everlong contract and approve Hyperdrive. + /// - `canRebalance()` should return true with a balance. + /// 2. Call `rebalance()` to open a position with all of balance. + /// - `canRebalance()` should return false with no balance. + /// 3. Increase block.timestamp until the position is mature. + /// - `canRebalance()` should return true with a mature position. + /// 4. Call `rebalance()` again to close matured positions + /// and use proceeds to open new position. + /// 5. `canRebalance()` should return false with no matured positions + /// and no balance. + function test_canRebalance_with_balance_matured_positions() external { + // Initialize the Hyperdrive instance by adding liquidity from Alice. + uint256 fixedRate = 0.5e18; + uint256 contribution = 10_000e18; + initialize(alice, fixedRate, contribution); + + // 1. Mint some tokens to Everlong for opening Longs. + // Ensure Everlong's balance is gte Hyperdrive's minTransactionAmount. + // Ensure `canRebalance()` returns true. + mintApproveHyperdriveBase(address(everlong), 100e18); + assertGe( + IERC20(everlong.asset()).balanceOf(address(everlong)), + hyperdrive.getPoolConfig().minimumTransactionAmount + ); + assertTrue( + everlong.canRebalance(), + "everlong should be able to rebalance when it has a balance > hyperdrive's minTransactionAmount" + ); + + // 2. Call `rebalance()` to cause Everlong to open a position. + // Ensure the `Rebalanced()` event is emitted. + // Ensure the position count is now 1. + // Ensure Everlong's balance is lt Hyperdrive's minTransactionAmount. + // Ensure `canRebalance()` returns false. + vm.expectEmit(true, true, true, true); + emit Rebalanced(); + everlong.rebalance(); + assertEq( + everlong.getPositionCount(), + 1, + "position count after first rebalance with balance should be 1" + ); + assertLt( + IERC20(everlong.asset()).balanceOf(address(everlong)), + hyperdrive.getPoolConfig().minimumTransactionAmount, + "everlong balance after first rebalance should be less than hyperdrive's minTransactionAmount" + ); + assertFalse( + everlong.canRebalance(), + "cannot rebalance without matured positions nor sufficient balance after first rebalance" + ); + + // 3. Increase block.timestamp until position is mature. + // Ensure Everlong has a matured position. + // Ensure `canRebalance()` returns true. + vm.warp(everlong.getPosition(0).maturityTime); + assertTrue( + everlong.hasMaturedPositions(), + "everlong should have matured position after calling warp" + ); + assertTrue( + everlong.canRebalance(), + "everlong should allow rebalance with matured position" + ); + + // 4. Call `rebalance()` to close mature position + // and open new position with proceeds. + // Ensure position count remains 1. + // Ensure Everlong does not have matured positions. + // Ensure Everlong does not have a balance > minTransactionAmount. + // Ensure `canRebalance()` returns false. + everlong.rebalance(); + assertEq( + everlong.getPositionCount(), + 1, + "position count after second rebalance with matured position should be 1" + ); + assertFalse( + everlong.hasMaturedPositions(), + "everlong should not have matured position after second rebalance" + ); + assertLt( + IERC20(everlong.asset()).balanceOf(address(everlong)), + hyperdrive.getPoolConfig().minimumTransactionAmount, + "everlong balance after second rebalance should be less than hyperdrive's minTransactionAmount" + ); + assertFalse( + everlong.canRebalance(), + "cannot rebalance without matured positions nor sufficient balance after second rebalance" + ); + } } diff --git a/test/units/IEverlong.t.sol b/test/units/IEverlong.t.sol deleted file mode 100644 index 256715e..0000000 --- a/test/units/IEverlong.t.sol +++ /dev/null @@ -1,169 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.20; - -import { IERC20 } from "openzeppelin/interfaces/IERC20.sol"; -import { EverlongTest } from "../harnesses/EverlongTest.sol"; - -/// @dev Extend only the test harness. -contract IEverlongTest is EverlongTest { - /// @dev Ensure that the `hyperdrive()` view function is implemented. - function test_hyperdrive() external view { - assertEq( - everlong.hyperdrive(), - address(hyperdrive), - "hyperdrive() should return hyperdrive address" - ); - } - - /// @dev Ensure that the `asset()` view function is implemented. - function test_asset() external view { - assertEq( - everlong.asset(), - address(hyperdrive.baseToken()), - "asset() should return hyperdrive base token address" - ); - } - - /// @dev Ensure that the `name()` view function is implemented. - function test_name() external view { - assertNotEq(everlong.name(), "", "name() not return an empty string"); - } - - /// @dev Ensure that the `symbol()` view function is implemented. - function test_symbol() external view { - assertNotEq( - everlong.symbol(), - "", - "symbol() not return an empty string" - ); - } - - /// @dev Ensure that the `kind()` view function is implemented. - function test_kind() external view { - assertNotEq(everlong.kind(), "", "kind is empty string"); - } - - /// @dev Ensure that the `version()` view function is implemented. - function test_version() external view { - assertNotEq(everlong.version(), "", "version is empty string"); - } - - /// @dev Ensure that `canRebalance()` returns false when everlong has - /// no positions nor balance. - function test_canRebalance_false_no_positions_no_balance() external { - // Check that Everlong: - // - has no positions - // - has no balance - // - `canRebalance()` returns false - assertEq( - everlong.getPositionCount(), - 0, - "everlong should not intialize with positions" - ); - assertEq( - IERC20(everlong.asset()).balanceOf(address(everlong)), - 0, - "everlong should not initialize with a balance" - ); - assertFalse( - everlong.canRebalance(), - "cannot rebalance without matured positions or balance" - ); - } - - /// @dev Ensures that `canRebalance()` only returns true with a balance - /// greater than or equal to Hyperdrive's minTransactionAmount or - /// with matured positions. - /// @dev The test goes through the following steps: - /// 1. Mint `asset` to the Everlong contract and approve Hyperdrive. - /// - `canRebalance()` should return true with a balance. - /// 2. Call `rebalance()` to open a position with all of balance. - /// - `canRebalance()` should return false with no balance. - /// 3. Increase block.timestamp until the position is mature. - /// - `canRebalance()` should return true with a mature position. - /// 4. Call `rebalance()` again to close matured positions - /// and use proceeds to open new position. - /// 5. `canRebalance()` should return false with no matured positions - /// and no balance. - function test_canRebalance_with_balance_matured_positions() external { - // Initialize the Hyperdrive instance by adding liquidity from Alice. - uint256 fixedRate = 0.5e18; - uint256 contribution = 10_000e18; - initialize(alice, fixedRate, contribution); - - // 1. Mint some tokens to Everlong for opening Longs. - // Ensure Everlong's balance is gte Hyperdrive's minTransactionAmount. - // Ensure `canRebalance()` returns true. - mintApproveHyperdriveBase(address(everlong), 100e18); - assertGe( - IERC20(everlong.asset()).balanceOf(address(everlong)), - hyperdrive.getPoolConfig().minimumTransactionAmount - ); - assertTrue( - everlong.canRebalance(), - "everlong should be able to rebalance when it has a balance > hyperdrive's minTransactionAmount" - ); - - // 2. Call `rebalance()` to cause Everlong to open a position. - // Ensure the `Rebalanced()` event is emitted. - // Ensure the position count is now 1. - // Ensure Everlong's balance is lt Hyperdrive's minTransactionAmount. - // Ensure `canRebalance()` returns false. - vm.expectEmit(true, true, true, true); - emit Rebalanced(); - everlong.rebalance(); - assertEq( - everlong.getPositionCount(), - 1, - "position count after first rebalance with balance should be 1" - ); - assertLt( - IERC20(everlong.asset()).balanceOf(address(everlong)), - hyperdrive.getPoolConfig().minimumTransactionAmount, - "everlong balance after first rebalance should be less than hyperdrive's minTransactionAmount" - ); - assertFalse( - everlong.canRebalance(), - "cannot rebalance without matured positions nor sufficient balance after first rebalance" - ); - - // 3. Increase block.timestamp until position is mature. - // Ensure Everlong has a matured position. - // Ensure `canRebalance()` returns true. - vm.warp(everlong.getPosition(0).maturityTime); - assertTrue( - everlong.hasMaturedPositions(), - "everlong should have matured position after calling warp" - ); - assertTrue( - everlong.canRebalance(), - "everlong should allow rebalance with matured position" - ); - - // 4. Call `rebalance()` to close mature position - // and open new position with proceeds. - // Ensure position count remains 1. - // Ensure Everlong does not have matured positions. - // Ensure Everlong does not have a balance > minTransactionAmount. - // Ensure `canRebalance()` returns false. - everlong.rebalance(); - assertEq( - everlong.getPositionCount(), - 1, - "position count after second rebalance with matured position should be 1" - ); - assertFalse( - everlong.hasMaturedPositions(), - "everlong should not have matured position after second rebalance" - ); - assertLt( - IERC20(everlong.asset()).balanceOf(address(everlong)), - hyperdrive.getPoolConfig().minimumTransactionAmount, - "everlong balance after second rebalance should be less than hyperdrive's minTransactionAmount" - ); - assertFalse( - everlong.canRebalance(), - "cannot rebalance without matured positions nor sufficient balance after second rebalance" - ); - } -} From 5ad7f29b72ee57dd4725276493d9b167652f8865 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Wed, 7 Aug 2024 14:22:09 -0500 Subject: [PATCH 07/27] fix Everlong test comment --- test/units/Everlong.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/units/Everlong.t.sol b/test/units/Everlong.t.sol index 610e5b8..b31d845 100644 --- a/test/units/Everlong.t.sol +++ b/test/units/Everlong.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import { EverlongTest } from "../harnesses/EverlongTest.sol"; -/// @dev +/// @dev Tests Everlong functionality. contract TestEverlong is EverlongTest { /// @dev Ensure that the `hyperdrive()` view function is implemented. function test_hyperdrive() external view { From a5f9e5bc1700829e9112b8cce0b7c82ec573d002 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Thu, 8 Aug 2024 01:12:04 -0500 Subject: [PATCH 08/27] maxDeposit functionality/tests and comment cleanup --- contracts/internal/EverlongERC4626.sol | 17 ++++- contracts/internal/EverlongStorage.sol | 2 +- test/harnesses/EverlongTest.sol | 4 +- test/units/EverlongERC4626.t.sol | 50 +++++++++++++-- test/units/EverlongPositions.t.sol | 87 +++++++++++--------------- 5 files changed, 101 insertions(+), 59 deletions(-) diff --git a/contracts/internal/EverlongERC4626.sol b/contracts/internal/EverlongERC4626.sol index 10b157b..8b593b2 100644 --- a/contracts/internal/EverlongERC4626.sol +++ b/contracts/internal/EverlongERC4626.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.22; +import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; +import { HyperdriveUtils } from "hyperdrive/test/utils/HyperdriveUtils.sol"; import { IERC20 } from "openzeppelin/interfaces/IERC20.sol"; import { ERC4626 } from "solady/tokens/ERC4626.sol"; import { EverlongPositions } from "./EverlongPositions.sol"; @@ -85,7 +87,16 @@ abstract contract EverlongERC4626 is ERC4626, EverlongPositions { return _virtualAssets; } - // TODO: Might not need this but including for convenience. + /// @inheritdoc ERC4626 + function maxDeposit( + address + ) public view override returns (uint256 maxAssets) { + maxAssets = HyperdriveUtils.calculateMaxLong(IHyperdrive(hyperdrive)); + } + + /// @dev Decrement the virtual assets and close sufficient positions to + /// service the withdrawal. + /// @param _assets Amount of assets owed to the withdrawer. function _beforeWithdraw( uint256 _assets, uint256 @@ -106,7 +117,9 @@ abstract contract EverlongERC4626 is ERC4626, EverlongPositions { } } - // TODO: Might not need this but including for convenience. + /// @dev Increment the virtual assets and rebalance positions to use + /// the newly-deposited liquidity. + /// @param _assets Amount of assets deposited. function _afterDeposit(uint256 _assets, uint256) internal virtual override { _virtualAssets += _assets; rebalance(); diff --git a/contracts/internal/EverlongStorage.sol b/contracts/internal/EverlongStorage.sol index 3b38454..007a126 100644 --- a/contracts/internal/EverlongStorage.sol +++ b/contracts/internal/EverlongStorage.sol @@ -21,7 +21,7 @@ abstract contract EverlongStorage is IEverlongEvents { // ── Hyperdrive ───────────────────────────────────────────── - /// @dev Address of the Hyperdrive instance wrapped by Everlong. + /// @notice Address of the Hyperdrive instance wrapped by Everlong. address public hyperdrive; /// @dev Whether to use Hyperdrive's base token to purchase bonds. diff --git a/test/harnesses/EverlongTest.sol b/test/harnesses/EverlongTest.sol index 83d9831..93c5164 100644 --- a/test/harnesses/EverlongTest.sol +++ b/test/harnesses/EverlongTest.sol @@ -78,8 +78,8 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { // TODO: This is gross, will refactor /// @dev Mint base token to the provided address a - /// and approve both the Everlong and Hyperdrive contract. - function mintApproveHyperdriveBase( + /// and approve the Everlong contract. + function mintApproveEverlongBaseAsset( address recipient, uint256 amount ) internal { diff --git a/test/units/EverlongERC4626.t.sol b/test/units/EverlongERC4626.t.sol index fecdaec..eef10be 100644 --- a/test/units/EverlongERC4626.t.sol +++ b/test/units/EverlongERC4626.t.sol @@ -27,7 +27,7 @@ contract TestEverlongERC4626 is EverlongTest { // Mint bob some assets and approve the Everlong contract. uint256 depositAmount = 1e18; - mintApproveHyperdriveBase(bob, depositAmount); + mintApproveEverlongBaseAsset(bob, depositAmount); // 1. Deposit assets into Everlong as Bob and confirm `Rebalanced` event // is emitted. @@ -68,7 +68,7 @@ contract TestEverlongERC4626 is EverlongTest { initialize(alice, fixedRate, contribution); // Deposit assets into Everlong as Bob. - mintApproveHyperdriveBase(bob, 10e18); + mintApproveEverlongBaseAsset(bob, 10e18); vm.startPrank(bob); everlong.deposit(1e18, bob); vm.stopPrank(); @@ -81,7 +81,7 @@ contract TestEverlongERC4626 is EverlongTest { assertFalse(everlong.hasMaturedPositions()); // Deposit assets into Everlong as Celine to create another position. - mintApproveHyperdriveBase(celine, 2e18); + mintApproveEverlongBaseAsset(celine, 2e18); vm.startPrank(celine); everlong.deposit(2e18, celine); vm.stopPrank(); @@ -116,7 +116,7 @@ contract TestEverlongERC4626 is EverlongTest { initialize(alice, fixedRate, contribution); // Mint Bob and Celine some assets and approve the Everlong contract. - mintApproveHyperdriveBase(bob, 10e18); + mintApproveEverlongBaseAsset(bob, 10e18); // Deposit assets into Everlong as Bob. vm.startPrank(bob); @@ -186,10 +186,52 @@ contract TestEverlongERC4626 is EverlongTest { ); } + /// @dev Validates that `maxDeposit(..)` returns a value that is + /// reasonable (less than uint256.max) and actually depositable. + function test_maxDeposit_is_depositable() external { + // Initialize the Hyperdrive instance by adding liquidity from Alice. + uint256 fixedRate = 0.5e18; + uint256 contribution = 10_000e18; + initialize(alice, fixedRate, contribution); + + // Ensure that `maxDeposit(Bob)` is less than uint256.max. + uint256 maxDeposit = everlong.maxDeposit(bob); + assertLt(maxDeposit, type(uint256).max); + + // Attempt to deposit `maxDeposit` as Bob. + mintApproveEverlongBaseAsset(bob, maxDeposit); + vm.startPrank(bob); + everlong.deposit(maxDeposit, bob); + vm.stopPrank(); + } + + /// @dev Validates that `maxDeposit(..)` decreases when a deposit is made. + function test_maxDeposit_decreases_after_deposit() external { + // Initialize the Hyperdrive instance by adding liquidity from Alice. + uint256 fixedRate = 0.5e18; + uint256 contribution = 10_000e18; + initialize(alice, fixedRate, contribution); + + // Make the maximum deposit as Bob. + uint256 maxDeposit = everlong.maxDeposit(bob); + mintApproveEverlongBaseAsset(bob, maxDeposit); + vm.startPrank(bob); + everlong.deposit(maxDeposit, bob); + vm.stopPrank(); + + // Ensure that the new maximum deposit is less than before. + assertLt( + everlong.maxDeposit(bob), + maxDeposit, + "max deposit should decrease after a deposit is made" + ); + } + /// @dev Validates that `_afterDeposit` increases total assets. function test_afterDeposit_virtual_asset_increase() external { // Call `_afterDeposit` with some assets. everlong.exposed_afterDeposit(5, 1); + // Ensure `totalAssets()` is increased by the correct amount. assertEq(everlong.totalAssets(), 5); } diff --git a/test/units/EverlongPositions.t.sol b/test/units/EverlongPositions.t.sol index b947a6e..d313a79 100644 --- a/test/units/EverlongPositions.t.sol +++ b/test/units/EverlongPositions.t.sol @@ -253,30 +253,18 @@ contract TestEverlongPositions is EverlongTest { ); } - /// @dev Ensures that `canRebalance()` only returns true with a balance - /// greater than or equal to Hyperdrive's minTransactionAmount or - /// with matured positions. - /// @dev The test goes through the following steps: - /// 1. Mint `asset` to the Everlong contract and approve Hyperdrive. - /// - `canRebalance()` should return true with a balance. - /// 2. Call `rebalance()` to open a position with all of balance. - /// - `canRebalance()` should return false with no balance. - /// 3. Increase block.timestamp until the position is mature. - /// - `canRebalance()` should return true with a mature position. - /// 4. Call `rebalance()` again to close matured positions - /// and use proceeds to open new position. - /// 5. `canRebalance()` should return false with no matured positions - /// and no balance. - function test_canRebalance_with_balance_matured_positions() external { + /// @dev Ensures that `canRebalance()` returns true with a balance + /// greater than Hyperdrive's minTransactionAmount. + function test_canRebalance_with_balance_over_min_tx_amount() external { // Initialize the Hyperdrive instance by adding liquidity from Alice. uint256 fixedRate = 0.5e18; uint256 contribution = 10_000e18; initialize(alice, fixedRate, contribution); - // 1. Mint some tokens to Everlong for opening Longs. + // Mint some tokens to Everlong for opening longs. // Ensure Everlong's balance is gte Hyperdrive's minTransactionAmount. // Ensure `canRebalance()` returns true. - mintApproveHyperdriveBase(address(everlong), 100e18); + mintApproveEverlongBaseAsset(address(everlong), 100e18); assertGe( IERC20(everlong.asset()).balanceOf(address(everlong)), hyperdrive.getPoolConfig().minimumTransactionAmount @@ -285,15 +273,27 @@ contract TestEverlongPositions is EverlongTest { everlong.canRebalance(), "everlong should be able to rebalance when it has a balance > hyperdrive's minTransactionAmount" ); + } + + /// @dev Ensures that `canRebalance()` returns false immediately after + /// a rebalance is performed. + function test_canRebalance_false_after_rebalance() external { + // Initialize the Hyperdrive instance by adding liquidity from Alice. + uint256 fixedRate = 0.5e18; + uint256 contribution = 10_000e18; + initialize(alice, fixedRate, contribution); - // 2. Call `rebalance()` to cause Everlong to open a position. + // Mint some tokens to Everlong for opening Longs. + // Call `rebalance()` to cause Everlong to open a position. // Ensure the `Rebalanced()` event is emitted. - // Ensure the position count is now 1. - // Ensure Everlong's balance is lt Hyperdrive's minTransactionAmount. - // Ensure `canRebalance()` returns false. + mintApproveEverlongBaseAsset(address(everlong), 100e18); vm.expectEmit(true, true, true, true); emit Rebalanced(); everlong.rebalance(); + + // Ensure the position count is now 1. + // Ensure Everlong's balance is lt Hyperdrive's minTransactionAmount. + // Ensure `canRebalance()` returns false. assertEq( everlong.getPositionCount(), 1, @@ -308,44 +308,31 @@ contract TestEverlongPositions is EverlongTest { everlong.canRebalance(), "cannot rebalance without matured positions nor sufficient balance after first rebalance" ); + } + + /// @dev Ensures that `canRebalance()` returns true with a matured + /// position. + function test_canRebalance_with_matured_position() external { + // Initialize the Hyperdrive instance by adding liquidity from Alice. + uint256 fixedRate = 0.5e18; + uint256 contribution = 10_000e18; + initialize(alice, fixedRate, contribution); - // 3. Increase block.timestamp until position is mature. + // Mint some tokens to Everlong for opening longs and rebalance. + mintApproveEverlongBaseAsset(address(everlong), 100e18); + everlong.rebalance(); + + // Increase block.timestamp until position is mature. // Ensure Everlong has a matured position. // Ensure `canRebalance()` returns true. - vm.warp(everlong.getPosition(0).maturityTime); + advanceTime(everlong.getPosition(0).maturityTime, 0); assertTrue( everlong.hasMaturedPositions(), - "everlong should have matured position after calling warp" + "everlong should have matured position after advancing time" ); assertTrue( everlong.canRebalance(), "everlong should allow rebalance with matured position" ); - - // 4. Call `rebalance()` to close mature position - // and open new position with proceeds. - // Ensure position count remains 1. - // Ensure Everlong does not have matured positions. - // Ensure Everlong does not have a balance > minTransactionAmount. - // Ensure `canRebalance()` returns false. - everlong.rebalance(); - assertEq( - everlong.getPositionCount(), - 1, - "position count after second rebalance with matured position should be 1" - ); - assertFalse( - everlong.hasMaturedPositions(), - "everlong should not have matured position after second rebalance" - ); - assertLt( - IERC20(everlong.asset()).balanceOf(address(everlong)), - hyperdrive.getPoolConfig().minimumTransactionAmount, - "everlong balance after second rebalance should be less than hyperdrive's minTransactionAmount" - ); - assertFalse( - everlong.canRebalance(), - "cannot rebalance without matured positions nor sufficient balance after second rebalance" - ); } } From 05461558f97910a2edcfe88fa3fed04c41551f44 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Thu, 8 Aug 2024 01:14:56 -0500 Subject: [PATCH 09/27] remove unused test helper --- test/harnesses/EverlongTest.sol | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/harnesses/EverlongTest.sol b/test/harnesses/EverlongTest.sol index 93c5164..7ef79e2 100644 --- a/test/harnesses/EverlongTest.sol +++ b/test/harnesses/EverlongTest.sol @@ -92,20 +92,6 @@ contract EverlongTest is HyperdriveTest, IEverlongEvents { vm.stopPrank(); } - // TODO: This is gross, will refactor - /// @dev Mint vault shares token to the provided address a - /// and approve both the Everlong and Hyperdrive contract. - function mintApproveHyperdriveShares( - address recipient, - uint256 amount - ) internal { - ERC20Mintable(hyperdrive.vaultSharesToken()).mint(recipient, amount); - ERC20Mintable(hyperdrive.vaultSharesToken()).approve( - address(everlong), - amount - ); - } - // ╭─────────────────────────────────────────────────────────╮ // │ Positions │ // ╰─────────────────────────────────────────────────────────╯ From 4bac426c161cb0393b17fca902dcf064895c137e Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Thu, 22 Aug 2024 16:14:48 -0500 Subject: [PATCH 10/27] addressing feedback: remove unnecessary approval + deposit override --- contracts/internal/EverlongERC4626.sol | 9 --------- 1 file changed, 9 deletions(-) diff --git a/contracts/internal/EverlongERC4626.sol b/contracts/internal/EverlongERC4626.sol index 8b593b2..15e72b5 100644 --- a/contracts/internal/EverlongERC4626.sol +++ b/contracts/internal/EverlongERC4626.sol @@ -38,15 +38,6 @@ abstract contract EverlongERC4626 is ERC4626, EverlongPositions { // │ Stateful │ // ╰─────────────────────────────────────────────────────────╯ - /// @inheritdoc ERC4626 - function deposit( - uint256 assets, - address to - ) public override returns (uint256 shares) { - IERC20(_asset).approve(hyperdrive, assets); - shares = super.deposit(assets, to); - } - /// @inheritdoc ERC4626 function redeem( uint256 shares, From 39153e1297c75b8d416bef59cad907ad9bf4882b Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Thu, 22 Aug 2024 16:29:19 -0500 Subject: [PATCH 11/27] addressing feedback: `redeem()` commenting --- contracts/internal/EverlongERC4626.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/internal/EverlongERC4626.sol b/contracts/internal/EverlongERC4626.sol index 15e72b5..1704d5e 100644 --- a/contracts/internal/EverlongERC4626.sol +++ b/contracts/internal/EverlongERC4626.sol @@ -44,7 +44,13 @@ abstract contract EverlongERC4626 is ERC4626, EverlongPositions { address to, address owner ) public override returns (uint256 assets) { + // Execute the original ERC4626 `redeem(...)` logic which includes + // calling the `_beforeWithdraw(...)` hook to ensure there is + // sufficient idle liquidity to service the redemption. assets = super.redeem(shares, to, owner); + + // Rebalance Everlong's positions by closing any matured positions + // and reinvesting the proceeds in new bonds. rebalance(); } From b7db8ca69d1d4df8c1e638c0eabf1b5d8b769180 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Thu, 22 Aug 2024 16:33:27 -0500 Subject: [PATCH 12/27] addressing feedback: _beforeWithdraw virtual assets commenting + todo --- contracts/internal/EverlongERC4626.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/internal/EverlongERC4626.sol b/contracts/internal/EverlongERC4626.sol index 1704d5e..24c7172 100644 --- a/contracts/internal/EverlongERC4626.sol +++ b/contracts/internal/EverlongERC4626.sol @@ -98,7 +98,13 @@ abstract contract EverlongERC4626 is ERC4626, EverlongPositions { uint256 _assets, uint256 ) internal virtual override { + // TODO: Re-evaluate this accounting logic after discussing + // withdrawal shares and whether to close immature positions. + // + // Decrement the virtual value of assets under Everlong control by the + // value of the assets being redeemed. _virtualAssets -= _assets; + // Check if Everlong has sufficient assets to service the withdrawal. if (IERC20(_asset).balanceOf(address(this)) < _assets) { // Everlong does not have sufficient assets to service withdrawal. From 4cc49515ee77b510b79943301fb66f4417438b8f Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Fri, 23 Aug 2024 09:39:24 -0500 Subject: [PATCH 13/27] addressing feedback: kind/version tests --- test/units/Everlong.t.sol | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/units/Everlong.t.sol b/test/units/Everlong.t.sol index b31d845..bf5640f 100644 --- a/test/units/Everlong.t.sol +++ b/test/units/Everlong.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; +import { EVERLONG_KIND, EVERLONG_VERSION } from "../../contracts/libraries/constants.sol"; import { EverlongTest } from "../harnesses/EverlongTest.sol"; /// @dev Tests Everlong functionality. @@ -16,11 +17,15 @@ contract TestEverlong is EverlongTest { /// @dev Ensure that the `kind()` view function is implemented. function test_kind() external view { - assertNotEq(everlong.kind(), "", "kind is empty string"); + assertNotEq(everlong.kind(), EVERLONG_KIND, "kind does not match"); } /// @dev Ensure that the `version()` view function is implemented. function test_version() external view { - assertNotEq(everlong.version(), "", "version is empty string"); + assertNotEq( + everlong.version(), + EVERLONG_VERSION, + "version does not match" + ); } } From dea4d2b5d1e2ac31d096b605157b20ce56f1898f Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Fri, 23 Aug 2024 09:40:22 -0500 Subject: [PATCH 14/27] Update test/units/EverlongERC4626.t.sol Co-authored-by: Alex Towle --- test/units/EverlongERC4626.t.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/units/EverlongERC4626.t.sol b/test/units/EverlongERC4626.t.sol index eef10be..a5c6d66 100644 --- a/test/units/EverlongERC4626.t.sol +++ b/test/units/EverlongERC4626.t.sol @@ -11,12 +11,12 @@ import { IEverlongEvents } from "../../contracts/interfaces/IEverlongEvents.sol" contract TestEverlongERC4626 is EverlongTest { /// @dev Validates behavior of the `deposit()` function for a single /// depositor. - /// @dev When calling the `deposit()` function... - /// 1. The `Rebalanced` event should be emitted - /// 2. The balance of the Everlong contract should be minimal. - /// 3. The share amount issued to the depositor should be equal to the + /// @dev When calling the `deposit()` function... + /// 1. The `Rebalanced` event should be emitted + /// 2. The balance of the Everlong contract should be minimal. + /// 3. The share amount issued to the depositor should be equal to the /// total supply of Everlong shares. - /// 4. The amount of assets deposited should relate to the share + /// 4. The amount of assets deposited should relate to the share /// count as follows: /// shares_issued = assets_deposited * (10 ^ decimalsOffset) function test_deposit_single_depositor() external { From 97f7523535a8e8ef65a133186ae39914ff73366a Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Fri, 23 Aug 2024 18:02:21 -0500 Subject: [PATCH 15/27] addressing feedback: refactor contracts to more "flat" structure (currently encountering issue with linearization of inheritance) --- .solhint.json | 1 + contracts/Everlong.sol | 67 ++++++++++++++++--- contracts/interfaces/IEverlong.sol | 4 ++ contracts/internal/EverlongAdmin.sol | 4 +- contracts/internal/EverlongBase.sol | 61 ++++------------- contracts/internal/EverlongERC4626.sol | 58 +++++----------- contracts/internal/EverlongPositions.sol | 80 +++++++++++++++-------- contracts/internal/EverlongStorage.sol | 7 +- test/exposed/EverlongBaseExposed.sol | 14 ---- test/exposed/EverlongERC4626Exposed.sol | 6 +- test/exposed/EverlongExposed.sol | 13 +++- test/exposed/EverlongPositionsExposed.sol | 27 ++++---- 12 files changed, 174 insertions(+), 168 deletions(-) delete mode 100644 test/exposed/EverlongBaseExposed.sol diff --git a/.solhint.json b/.solhint.json index 7e00905..d1d6fa7 100644 --- a/.solhint.json +++ b/.solhint.json @@ -3,6 +3,7 @@ "rules": { "func-name-mixedcase": "off", "var-name-mixedcase": "off", + "immutable-vars-naming": "off", "no-unused-vars": "error" } } diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index ad92cff..f660049 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -1,7 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.22; +import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; +import { IEverlong } from "./interfaces/IEverlong.sol"; +import { EverlongAdmin } from "./internal/EverlongAdmin.sol"; import { EverlongBase } from "./internal/EverlongBase.sol"; +import { EverlongERC4626 } from "./internal/EverlongERC4626.sol"; +import { EverlongPositions } from "./internal/EverlongPositions.sol"; +import { EVERLONG_KIND, EVERLONG_VERSION } from "./libraries/Constants.sol"; /// ,---..-. .-.,---. ,---. ,-. .---. .-. .-. ,--, /// | .-' \ \ / / | .-' | .-.\ | | / .-. ) | \| |.' .' @@ -60,16 +66,59 @@ import { EverlongBase } from "./internal/EverlongBase.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -contract Everlong is EverlongBase { - /// @notice Initial configuration paramters for Everlong. - /// @param _name Name of the ERC20 token managed by Everlong. - /// @param _symbol Symbol of the ERC20 token managed by Everlong. - /// @param __hyperdrive Address of the Hyperdrive instance wrapped by Everlong. - /// @param __asBase Whether to use Hyperdrive's base token for bond purchases. +contract Everlong is + IEverlong, + EverlongBase, + EverlongAdmin, + EverlongPositions, + EverlongERC4626 +{ + // ╭─────────────────────────────────────────────────────────╮ + // │ Constructor │ + // ╰─────────────────────────────────────────────────────────╯ + + /// @notice Initial configuration paramters for EverlongERC4626. + /// @param __name Name of the ERC20 token managed by Everlong. + /// @param __symbol Symbol of the ERC20 token managed by Everlong. + /// @param __hyperdrive Address of the Hyperdrive instance. + /// @param __asBase Whether to use the base or shares token from Hyperdrive. constructor( - string memory _name, - string memory _symbol, + string memory __name, + string memory __symbol, address __hyperdrive, bool __asBase - ) EverlongBase(_name, _symbol, __hyperdrive, __asBase) {} + ) { + // Store constructor parameters. + _name = __name; + _symbol = __symbol; + _hyperdrive = __hyperdrive; + _asBase = __asBase; + _asset = __asBase + ? IHyperdrive(__hyperdrive).baseToken() + : IHyperdrive(__hyperdrive).vaultSharesToken(); + + // Attempt to retrieve the decimals from the {_asset} contract. + // If it does not implement `decimals() (uint256)`, use the default. + (bool success, uint8 result) = _tryGetAssetDecimals(_asset); + _decimals = success ? result : _DEFAULT_UNDERLYING_DECIMALS; + } + + // ╭─────────────────────────────────────────────────────────╮ + // │ Getters │ + // ╰─────────────────────────────────────────────────────────╯ + + /// @inheritdoc IEverlong + function kind() external view returns (string memory) { + return EVERLONG_KIND; + } + + /// @inheritdoc IEverlong + function version() external view returns (string memory) { + return EVERLONG_VERSION; + } + + /// @inheritdoc IEverlong + function hyperdrive() external view returns (address) { + return _hyperdrive; + } } diff --git a/contracts/interfaces/IEverlong.sol b/contracts/interfaces/IEverlong.sol index 6d9efbf..f37f3a9 100644 --- a/contracts/interfaces/IEverlong.sol +++ b/contracts/interfaces/IEverlong.sol @@ -45,4 +45,8 @@ interface IEverlong is /// @notice Thrown when attempting to close a position with /// a `bondAmount` greater than that contained by the position. error InconsistentPositionBondAmount(); + + /// @notice Thrown when a target idle amount is too high to be reached + /// even after closing all positions. + error TargetIdleTooHigh(); } diff --git a/contracts/internal/EverlongAdmin.sol b/contracts/internal/EverlongAdmin.sol index 60a82d0..95bda9a 100644 --- a/contracts/internal/EverlongAdmin.sol +++ b/contracts/internal/EverlongAdmin.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.22; import { IEverlong } from "../interfaces/IEverlong.sol"; import { IEverlongAdmin } from "../interfaces/IEverlongAdmin.sol"; -import { EverlongStorage } from "./EverlongStorage.sol"; +import { EverlongBase } from "../internal/EverlongBase.sol"; /// @author DELV /// @title EverlongAdmin @@ -11,7 +11,7 @@ import { EverlongStorage } from "./EverlongStorage.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -abstract contract EverlongAdmin is EverlongStorage, IEverlongAdmin { +abstract contract EverlongAdmin is EverlongBase, IEverlongAdmin { // ╭─────────────────────────────────────────────────────────╮ // │ Modifiers │ // ╰─────────────────────────────────────────────────────────╯ diff --git a/contracts/internal/EverlongBase.sol b/contracts/internal/EverlongBase.sol index fcc542a..6307691 100644 --- a/contracts/internal/EverlongBase.sol +++ b/contracts/internal/EverlongBase.sol @@ -1,12 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.22; -import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; import { DoubleEndedQueue } from "openzeppelin/utils/structs/DoubleEndedQueue.sol"; -import { IERC20 } from "openzeppelin/interfaces/IERC20.sol"; -import { EVERLONG_KIND, EVERLONG_VERSION } from "../libraries/Constants.sol"; -import { EverlongAdmin } from "./EverlongAdmin.sol"; -import { EverlongERC4626 } from "./EverlongERC4626.sol"; +import { EverlongStorage } from "./EverlongStorage.sol"; // TODO: Reassess whether centralized configuration management makes sense. // https://github.com/delvtech/everlong/pull/2#discussion_r1703799747 @@ -16,54 +12,21 @@ import { EverlongERC4626 } from "./EverlongERC4626.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -abstract contract EverlongBase is EverlongAdmin, EverlongERC4626 { +abstract contract EverlongBase is EverlongStorage { using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; // ╭─────────────────────────────────────────────────────────╮ - // │ Constructor │ + // │ Stateful │ // ╰─────────────────────────────────────────────────────────╯ - /// @notice Initial configuration paramters for Everlong. - /// @param _name Name of the ERC20 token managed by Everlong. - /// @param _symbol Symbol of the ERC20 token managed by Everlong. - /// @param _hyperdrive Address of the Hyperdrive instance wrapped by Everlong. - /// @param __asBase Whether to use Hyperdrive's base token for bond purchases. - constructor( - string memory _name, - string memory _symbol, - address _hyperdrive, - bool __asBase - ) - EverlongERC4626( - _name, - _symbol, - __asBase - ? IHyperdrive(_hyperdrive).baseToken() - : IHyperdrive(_hyperdrive).vaultSharesToken() - ) - { - // Store constructor parameters. - hyperdrive = _hyperdrive; - _asBase = __asBase; - _admin = msg.sender; + /// @dev Rebalances the Everlong bond portfolio if needed. + function _rebalance() public virtual; - // Give 1 wei approval to make the slot "dirty". - IERC20(_asset).approve(_hyperdrive, 1); - } - - // ╭─────────────────────────────────────────────────────────╮ - // │ Getters │ - // ╰─────────────────────────────────────────────────────────╯ - - /// @notice Returns the kind of the Everlong instance. - /// @return Everlong contract kind. - function kind() public view virtual returns (string memory) { - return EVERLONG_KIND; - } - - /// @notice Returns the version of the Everlong instance. - /// @return Everlong contract version. - function version() public view virtual returns (string memory) { - return EVERLONG_VERSION; - } + /// @dev Close positions until sufficient idle liquidity is held. + /// @dev Reverts if the target is unreachable. + /// @param _target Target amount of idle liquidity to reach. + /// @return idle Amount of idle after the increase. + function _increaseIdle( + uint256 _target + ) internal virtual returns (uint256 idle); } diff --git a/contracts/internal/EverlongERC4626.sol b/contracts/internal/EverlongERC4626.sol index 24c7172..211aedb 100644 --- a/contracts/internal/EverlongERC4626.sol +++ b/contracts/internal/EverlongERC4626.sol @@ -3,9 +3,8 @@ pragma solidity 0.8.22; import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; import { HyperdriveUtils } from "hyperdrive/test/utils/HyperdriveUtils.sol"; -import { IERC20 } from "openzeppelin/interfaces/IERC20.sol"; import { ERC4626 } from "solady/tokens/ERC4626.sol"; -import { EverlongPositions } from "./EverlongPositions.sol"; +import { EverlongBase } from "./EverlongBase.sol"; /// @author DELV /// @title EverlongERC4626 @@ -13,27 +12,7 @@ import { EverlongPositions } from "./EverlongPositions.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -abstract contract EverlongERC4626 is ERC4626, EverlongPositions { - // ╭─────────────────────────────────────────────────────────╮ - // │ Constructor │ - // ╰─────────────────────────────────────────────────────────╯ - - /// @notice Initial configuration paramters for EverlongERC4626. - /// @param __name Name of the ERC20 token managed by Everlong. - /// @param __symbol Symbol of the ERC20 token managed by Everlong. - /// @param __asset Base token used by Everlong for deposits/withdrawals. - constructor(string memory __name, string memory __symbol, address __asset) { - // Store constructor parameters. - _name = __name; - _symbol = __symbol; - _asset = __asset; - - // Attempt to retrieve the decimals from the {_asset} contract. - // If it does not implement `decimals() (uint256)`, use the default. - (bool success, uint8 result) = _tryGetAssetDecimals(__asset); - _decimals = success ? result : _DEFAULT_UNDERLYING_DECIMALS; - } - +abstract contract EverlongERC4626 is ERC4626, EverlongBase { // ╭─────────────────────────────────────────────────────────╮ // │ Stateful │ // ╰─────────────────────────────────────────────────────────╯ @@ -51,7 +30,7 @@ abstract contract EverlongERC4626 is ERC4626, EverlongPositions { // Rebalance Everlong's positions by closing any matured positions // and reinvesting the proceeds in new bonds. - rebalance(); + _rebalance(); } // TODO: Implement. @@ -88,7 +67,7 @@ abstract contract EverlongERC4626 is ERC4626, EverlongPositions { function maxDeposit( address ) public view override returns (uint256 maxAssets) { - maxAssets = HyperdriveUtils.calculateMaxLong(IHyperdrive(hyperdrive)); + maxAssets = HyperdriveUtils.calculateMaxLong(IHyperdrive(_hyperdrive)); } /// @dev Decrement the virtual assets and close sufficient positions to @@ -101,31 +80,28 @@ abstract contract EverlongERC4626 is ERC4626, EverlongPositions { // TODO: Re-evaluate this accounting logic after discussing // withdrawal shares and whether to close immature positions. // - // Decrement the virtual value of assets under Everlong control by the - // value of the assets being redeemed. + // Increase the virtual value of Everlong controlled assets by the + // amount deposited. _virtualAssets -= _assets; - // Check if Everlong has sufficient assets to service the withdrawal. - if (IERC20(_asset).balanceOf(address(this)) < _assets) { - // Everlong does not have sufficient assets to service withdrawal. - // First try closing all mature positions. If sufficient to - // service the withdrawal, then continue. - _closeMaturedPositions(); - uint256 _currentBalance = IERC20(_asset).balanceOf(address(this)); - if (_currentBalance >= _assets) return; - - // Close remaining positions until Everlong's balance is - // enough to meet the withdrawal. - _closePositionsByOutput(_assets - _currentBalance); - } + // Close remaining positions until Everlong's balance is enough to + // meet the withdrawal. + _increaseIdle(_assets); } /// @dev Increment the virtual assets and rebalance positions to use /// the newly-deposited liquidity. /// @param _assets Amount of assets deposited. function _afterDeposit(uint256 _assets, uint256) internal virtual override { + // TODO: Re-evaluate this accounting logic after discussing + // withdrawal shares and whether to close immature positions. + // + // Increase the virtual value of Everlong controlled assets by the + // amount deposited. _virtualAssets += _assets; - rebalance(); + + // Rebalance the Everlong portfolio. + _rebalance(); } // ╭─────────────────────────────────────────────────────────╮ diff --git a/contracts/internal/EverlongPositions.sol b/contracts/internal/EverlongPositions.sol index dfd98c3..5be325f 100644 --- a/contracts/internal/EverlongPositions.sol +++ b/contracts/internal/EverlongPositions.sol @@ -7,7 +7,7 @@ import { IERC20 } from "openzeppelin/interfaces/IERC20.sol"; import { IEverlong } from "../interfaces/IEverlong.sol"; import { IEverlongPositions } from "../interfaces/IEverlongPositions.sol"; import { Position } from "../types/Position.sol"; -import { EverlongStorage } from "./EverlongStorage.sol"; +import { EverlongBase } from "./EverlongBase.sol"; /// @author DELV /// @title EverlongPositions @@ -15,7 +15,7 @@ import { EverlongStorage } from "./EverlongStorage.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -abstract contract EverlongPositions is EverlongStorage, IEverlongPositions { +abstract contract EverlongPositions is EverlongBase, IEverlongPositions { using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; // ╭─────────────────────────────────────────────────────────╮ @@ -23,7 +23,12 @@ abstract contract EverlongPositions is EverlongStorage, IEverlongPositions { // ╰─────────────────────────────────────────────────────────╯ /// @inheritdoc IEverlongPositions - function rebalance() public { + function rebalance() external { + _rebalance(); + } + + /// @dev Rebalances the Everlong bond portfolio if needed. + function _rebalance() public override { // Close all mature positions (if present) so that the proceeds can be // used to purchase longs. if (hasMaturedPositions()) { @@ -102,8 +107,8 @@ abstract contract EverlongPositions is EverlongStorage, IEverlongPositions { // TODO: Ensure amount < maxLongAmount // TODO: Idle liquidity implementation uint256 _amount = _excessLiquidity(); - IERC20(_asset).approve(hyperdrive, _amount); - (uint256 _maturityTime, uint256 _bondAmount) = IHyperdrive(hyperdrive) + IERC20(_asset).approve(_hyperdrive, _amount); + (uint256 _maturityTime, uint256 _bondAmount) = IHyperdrive(_hyperdrive) .openLong( _amount, _minOpenLongOutput(_amount), @@ -169,35 +174,52 @@ abstract contract EverlongPositions is EverlongStorage, IEverlongPositions { // │ Position Closing (Internal) │ // ╰─────────────────────────────────────────────────────────╯ - /// @dev Close positions until the minimum output is met. - /// @param _minOutput Output needed from closed positions. - function _closePositionsByOutput(uint256 _minOutput) internal { - // Loop through positions and close them all. - // TODO: Enable closing of positions incrementally to avoid - // the case where the # of mature positions exceeds the max - // gas per block. - Position memory _position; - uint256 _output; - while (_output < _minOutput) { - // Retrieve the oldest position and close it. - _position = getPosition(0); - _output += IHyperdrive(hyperdrive).closeLong( - _position.maturityTime, - _position.bondAmount, - _minCloseLongOutput( - _position.maturityTime, - _position.bondAmount - ), + /// @inheritdoc EverlongBase + function _increaseIdle( + uint256 _target + ) internal override returns (uint256 idle) { + // Obtain the current amount of idle held by Everlong and return if + // it is above the target. + idle = IERC20(_asset).balanceOf(address(this)); + if (idle > _target) return idle; + + // Close all matured positions and return if updated idle is above + // the target. + idle += _closeMaturedPositions(); + if (idle > _target) return idle; + + // Close immature positions from oldest to newest until idle is + // above the target. + uint256 positionCount = _positions.length(); + Position memory position; + while (positionCount > 0) { + position = getPosition(0); + + // Close the position and add output to the current idle. + idle += IHyperdrive(_hyperdrive).closeLong( + position.maturityTime, + position.bondAmount, + _minCloseLongOutput(position.maturityTime, position.bondAmount), IHyperdrive.Options(address(this), _asBase, "") ); - // Update positions to reflect the newly closed long. - _handleCloseLong(uint128(_position.bondAmount)); + // Update accounting for the closed position. + _handleCloseLong(uint128(position.bondAmount)); + + // Return if the updated idle is above the target. + if (idle > _target) return idle; + + positionCount--; } + + // Revert since all positions are closed and the target idle is + // has not been met; + revert IEverlong.TargetIdleTooHigh(); } /// @dev Close all matured positions. - function _closeMaturedPositions() internal { + /// @return output Output received from closing the positions. + function _closeMaturedPositions() internal returns (uint256 output) { // Loop through mature positions and close them all. // TODO: Enable closing of mature positions incrementally to avoid // the case where the # of mature positions exceeds the max @@ -206,7 +228,7 @@ abstract contract EverlongPositions is EverlongStorage, IEverlongPositions { while (hasMaturedPositions()) { // Retrieve the oldest matured position and close it. _position = getPosition(0); - IHyperdrive(hyperdrive).closeLong( + output += IHyperdrive(_hyperdrive).closeLong( _position.maturityTime, _position.bondAmount, _minCloseLongOutput( @@ -315,7 +337,7 @@ abstract contract EverlongPositions is EverlongStorage, IEverlongPositions { // Hyperdrive's minimum transaction amount. return _excessLiquidity() >= - IHyperdrive(hyperdrive).getPoolConfig().minimumTransactionAmount; + IHyperdrive(_hyperdrive).getPoolConfig().minimumTransactionAmount; } // TODO: Consider storing hyperdrive's minimumTransactionAmount. diff --git a/contracts/internal/EverlongStorage.sol b/contracts/internal/EverlongStorage.sol index 007a126..4317753 100644 --- a/contracts/internal/EverlongStorage.sol +++ b/contracts/internal/EverlongStorage.sol @@ -22,15 +22,16 @@ abstract contract EverlongStorage is IEverlongEvents { // ── Hyperdrive ───────────────────────────────────────────── /// @notice Address of the Hyperdrive instance wrapped by Everlong. - address public hyperdrive; + address immutable _hyperdrive; /// @dev Whether to use Hyperdrive's base token to purchase bonds. // If false, use the Hyperdrive's `vaultSharesToken`. - bool internal _asBase; + bool immutable _asBase; // ── Positions ────────────────────────────────────────────── // TODO: Reassess using a more tailored data structure. + // /// @dev Utility data structure to manage the position queue. /// Supports pushing and popping from both the front and back. DoubleEndedQueue.Bytes32Deque internal _positions; @@ -46,7 +47,7 @@ abstract contract EverlongStorage is IEverlongEvents { uint8 public constant decimalsOffset = 3; /// @dev Address of the token to use for Hyperdrive bond purchase/close. - address internal _asset; + address immutable _asset; // TODO: Remove in favor of more sophisticated position valuation. // TODO: Use some SafeMath library. diff --git a/test/exposed/EverlongBaseExposed.sol b/test/exposed/EverlongBaseExposed.sol deleted file mode 100644 index 4e2ddb8..0000000 --- a/test/exposed/EverlongBaseExposed.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.20; - -import { EverlongBase } from "../../contracts/internal/EverlongBase.sol"; -import { EverlongAdminExposed } from "./EverlongAdminExposed.sol"; -import { EverlongERC4626Exposed } from "./EverlongERC4626Exposed.sol"; - -/// @title EverlongBaseExposed -/// @dev Exposes all internal functions for the `EverlongBase` contract. -abstract contract EverlongBaseExposed is - EverlongAdminExposed, - EverlongERC4626Exposed, - EverlongBase -{} diff --git a/test/exposed/EverlongERC4626Exposed.sol b/test/exposed/EverlongERC4626Exposed.sol index 7195ad4..39b95cb 100644 --- a/test/exposed/EverlongERC4626Exposed.sol +++ b/test/exposed/EverlongERC4626Exposed.sol @@ -1,15 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; -import { EverlongPositionsExposed } from "../exposed/EverlongPositionsExposed.sol"; import { EverlongERC4626 } from "../../contracts/internal/EverlongERC4626.sol"; /// @title EverlongERC4626Exposed /// @dev Exposes all internal functions for the `EverlongERC4626` contract. -abstract contract EverlongERC4626Exposed is - EverlongERC4626, - EverlongPositionsExposed -{ +abstract contract EverlongERC4626Exposed is EverlongERC4626 { function exposed_beforeWithdraw(uint256 _assets, uint256 _shares) public { return _beforeWithdraw(_assets, _shares); } diff --git a/test/exposed/EverlongExposed.sol b/test/exposed/EverlongExposed.sol index a9f51fd..bd8d02e 100644 --- a/test/exposed/EverlongExposed.sol +++ b/test/exposed/EverlongExposed.sol @@ -3,11 +3,20 @@ pragma solidity ^0.8.20; import { Test } from "forge-std/Test.sol"; import { Everlong } from "../../contracts/Everlong.sol"; -import { EverlongBaseExposed } from "./EverlongBaseExposed.sol"; + +import { EverlongAdminExposed } from "./EverlongAdminExposed.sol"; +import { EverlongERC4626Exposed } from "./EverlongERC4626Exposed.sol"; +import { EverlongPositionsExposed } from "./EverlongPositionsExposed.sol"; /// @title EverlongExposed /// @dev Exposes all internal functions for the `Everlong` contract. -contract EverlongExposed is EverlongBaseExposed, Everlong, Test { +contract EverlongExposed is + EverlongAdminExposed, + EverlongERC4626Exposed, + EverlongPositionsExposed, + Everlong, + Test +{ /// @notice Initial configuration paramters for Everlong. /// @param hyperdrive_ Address of the Hyperdrive instance wrapped by Everlong. /// @param name_ Name of the ERC20 token managed by Everlong. diff --git a/test/exposed/EverlongPositionsExposed.sol b/test/exposed/EverlongPositionsExposed.sol index e2131fe..6b9d98c 100644 --- a/test/exposed/EverlongPositionsExposed.sol +++ b/test/exposed/EverlongPositionsExposed.sol @@ -6,18 +6,6 @@ import { EverlongPositions } from "../../contracts/internal/EverlongPositions.so /// @title EverlongPositionsExposed /// @dev Exposes all internal functions for the `EverlongPositions` contract. abstract contract EverlongPositionsExposed is EverlongPositions { - /// @notice Initial configuration paramters for Everlong. - /// @param hyperdrive_ Address of the Hyperdrive instance wrapped by Everlong. - /// @param name_ Name of the ERC20 token managed by Everlong. - /// @param symbol_ Symbol of the ERC20 token managed by Everlong. - /// @param asBase_ Whether to use Hyperdrive's base token for bond purchases. - // constructor( - // string memory name_, - // string memory symbol_, - // address hyperdrive_, - // bool asBase_ - // ) EverlongBase(name_, symbol_, hyperdrive_, asBase_) {} - /// @notice Calculates the amount of excess liquidity that can be spent opening longs. /// @notice Can be overridden by child contracts. /// @return Amount of excess liquidity that can be spent opening longs. @@ -76,9 +64,20 @@ abstract contract EverlongPositionsExposed is EverlongPositions { return _handleOpenLong(_maturityTime, _bondAmountPurchased); } + /// @dev Close positions until sufficient idle liquidity is held. + /// @dev Reverts if the target is unreachable. + /// @param _target Target amount of idle liquidity to reach. + /// @return idle Amount of idle after the increase. + function exposed_increaseIdle( + uint256 _target + ) internal returns (uint256 idle) { + return _increaseIdle(_target); + } + /// @notice Close all matured positions. - function exposed_closeMaturedPositions() public { - return _closeMaturedPositions(); + /// @return output Output received from closing the positions. + function exposed_closeMaturedPositions() public returns (uint256 output) { + output = _closeMaturedPositions(); } /// @notice Account for closed bonds at the oldest `maturityTime` From bf17ef3283ae5494e6127622b148e5bb36209f1d Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Sun, 25 Aug 2024 18:26:01 -0500 Subject: [PATCH 16/27] addressing feedback: remove IEverlong from Everlong inheritance --- contracts/Everlong.sol | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index f660049..510a154 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -66,13 +66,7 @@ import { EVERLONG_KIND, EVERLONG_VERSION } from "./libraries/Constants.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -contract Everlong is - IEverlong, - EverlongBase, - EverlongAdmin, - EverlongPositions, - EverlongERC4626 -{ +contract Everlong is EverlongAdmin, EverlongPositions, EverlongERC4626 { // ╭─────────────────────────────────────────────────────────╮ // │ Constructor │ // ╰─────────────────────────────────────────────────────────╯ @@ -107,17 +101,19 @@ contract Everlong is // │ Getters │ // ╰─────────────────────────────────────────────────────────╯ - /// @inheritdoc IEverlong + /// @notice Gets the Everlong instance's kind. + /// @return The Everlong instance's kind. function kind() external view returns (string memory) { return EVERLONG_KIND; } - /// @inheritdoc IEverlong + /// @notice Gets the Everlong instance's version. + /// @return The Everlong instance's version. function version() external view returns (string memory) { return EVERLONG_VERSION; } - /// @inheritdoc IEverlong + /// @notice Gets the address of the underlying Hyperdrive Instance function hyperdrive() external view returns (address) { return _hyperdrive; } From cfcf9c5bb13ca4acb6c28e6799808f0000521244 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Sun, 25 Aug 2024 21:01:43 -0500 Subject: [PATCH 17/27] addressing feedback: remove duplicated test + clean up inheritance --- contracts/internal/EverlongBase.sol | 3 ++- contracts/internal/EverlongStorage.sol | 9 ++++----- test/units/Everlong.t.sol | 4 ++-- test/units/EverlongAdmin.t.sol | 9 ++++----- test/units/EverlongERC4626.t.sol | 9 --------- 5 files changed, 12 insertions(+), 22 deletions(-) diff --git a/contracts/internal/EverlongBase.sol b/contracts/internal/EverlongBase.sol index 6307691..1239450 100644 --- a/contracts/internal/EverlongBase.sol +++ b/contracts/internal/EverlongBase.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.22; import { DoubleEndedQueue } from "openzeppelin/utils/structs/DoubleEndedQueue.sol"; +import { IEverlongEvents } from "../interfaces/IEverlongEvents.sol"; import { EverlongStorage } from "./EverlongStorage.sol"; // TODO: Reassess whether centralized configuration management makes sense. @@ -12,7 +13,7 @@ import { EverlongStorage } from "./EverlongStorage.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -abstract contract EverlongBase is EverlongStorage { +abstract contract EverlongBase is EverlongStorage, IEverlongEvents { using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; // ╭─────────────────────────────────────────────────────────╮ diff --git a/contracts/internal/EverlongStorage.sol b/contracts/internal/EverlongStorage.sol index 4317753..9e8a7a6 100644 --- a/contracts/internal/EverlongStorage.sol +++ b/contracts/internal/EverlongStorage.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.22; import { DoubleEndedQueue } from "openzeppelin/utils/structs/DoubleEndedQueue.sol"; -import { IEverlongEvents } from "../interfaces/IEverlongEvents.sol"; // TODO: Reassess whether centralized configuration management makes sense. // https://github.com/delvtech/everlong/pull/2#discussion_r1703799747 @@ -12,7 +11,7 @@ import { IEverlongEvents } from "../interfaces/IEverlongEvents.sol"; /// @custom:disclaimer The language used in this code is for coding convenience /// only, and is not intended to, and does not, have any /// particular legal or regulatory significance. -abstract contract EverlongStorage is IEverlongEvents { +abstract contract EverlongStorage { using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; // ── Admin ────────────────────────────────────────────────── @@ -22,11 +21,11 @@ abstract contract EverlongStorage is IEverlongEvents { // ── Hyperdrive ───────────────────────────────────────────── /// @notice Address of the Hyperdrive instance wrapped by Everlong. - address immutable _hyperdrive; + address internal immutable _hyperdrive; /// @dev Whether to use Hyperdrive's base token to purchase bonds. // If false, use the Hyperdrive's `vaultSharesToken`. - bool immutable _asBase; + bool internal immutable _asBase; // ── Positions ────────────────────────────────────────────── @@ -47,7 +46,7 @@ abstract contract EverlongStorage is IEverlongEvents { uint8 public constant decimalsOffset = 3; /// @dev Address of the token to use for Hyperdrive bond purchase/close. - address immutable _asset; + address internal immutable _asset; // TODO: Remove in favor of more sophisticated position valuation. // TODO: Use some SafeMath library. diff --git a/test/units/Everlong.t.sol b/test/units/Everlong.t.sol index bf5640f..b8fe73a 100644 --- a/test/units/Everlong.t.sol +++ b/test/units/Everlong.t.sol @@ -17,12 +17,12 @@ contract TestEverlong is EverlongTest { /// @dev Ensure that the `kind()` view function is implemented. function test_kind() external view { - assertNotEq(everlong.kind(), EVERLONG_KIND, "kind does not match"); + assertEq(everlong.kind(), EVERLONG_KIND, "kind does not match"); } /// @dev Ensure that the `version()` view function is implemented. function test_version() external view { - assertNotEq( + assertEq( everlong.version(), EVERLONG_VERSION, "version does not match" diff --git a/test/units/EverlongAdmin.t.sol b/test/units/EverlongAdmin.t.sol index 357b2f7..c120669 100644 --- a/test/units/EverlongAdmin.t.sol +++ b/test/units/EverlongAdmin.t.sol @@ -18,10 +18,9 @@ contract TestEverlongAdmin is EverlongTest { /// @dev Validates successful `setAdmin` call by current `admin`. function test_setAdmin_success_deployer() external { // Ensure that the deployer can set the admin address. - vm.expectEmit(true, false, false, true); - emit AdminUpdated(address(0)); - vm.startPrank(deployer); - everlong.setAdmin(address(0)); - assertEq(everlong.admin(), address(0), "admin address not updated"); + vm.expectEmit(true, true, true, true); + emit AdminUpdated(alice); + everlong.setAdmin(alice); + assertEq(everlong.admin(), alice, "admin address not updated"); } } diff --git a/test/units/EverlongERC4626.t.sol b/test/units/EverlongERC4626.t.sol index a5c6d66..3b56d02 100644 --- a/test/units/EverlongERC4626.t.sol +++ b/test/units/EverlongERC4626.t.sol @@ -154,15 +154,6 @@ contract TestEverlongERC4626 is EverlongTest { assertEq(everlong.getPositionCount(), 1); } - /// @dev Ensure that the `hyperdrive()` view function is implemented. - function test_hyperdrive() external view { - assertEq( - everlong.hyperdrive(), - address(hyperdrive), - "hyperdrive() should return hyperdrive address" - ); - } - /// @dev Ensure that the `asset()` view function is implemented. function test_asset() external view { assertEq( From cc984ff9bb1b07bb97d2e20bd786973b1b3d45a1 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Sun, 25 Aug 2024 21:03:54 -0500 Subject: [PATCH 18/27] _rebalance() visibility --- contracts/internal/EverlongBase.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/internal/EverlongBase.sol b/contracts/internal/EverlongBase.sol index 1239450..4e93098 100644 --- a/contracts/internal/EverlongBase.sol +++ b/contracts/internal/EverlongBase.sol @@ -21,7 +21,7 @@ abstract contract EverlongBase is EverlongStorage, IEverlongEvents { // ╰─────────────────────────────────────────────────────────╯ /// @dev Rebalances the Everlong bond portfolio if needed. - function _rebalance() public virtual; + function _rebalance() internal virtual; /// @dev Close positions until sufficient idle liquidity is held. /// @dev Reverts if the target is unreachable. From 887536066dbdf1b2d18dfea5e7ad17b9fcb439ff Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Sun, 25 Aug 2024 21:19:33 -0500 Subject: [PATCH 19/27] addressing feedback: immutable decimals + fix `_increaseIdle` logic + set admin in constructor --- contracts/Everlong.sol | 3 +++ contracts/internal/EverlongPositions.sol | 8 ++++---- contracts/internal/EverlongStorage.sol | 2 +- test/units/EverlongAdmin.t.sol | 1 + 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index 510a154..0dec7b0 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -91,6 +91,9 @@ contract Everlong is EverlongAdmin, EverlongPositions, EverlongERC4626 { ? IHyperdrive(__hyperdrive).baseToken() : IHyperdrive(__hyperdrive).vaultSharesToken(); + // Set the admin to the contract deployer. + _admin = msg.sender; + // Attempt to retrieve the decimals from the {_asset} contract. // If it does not implement `decimals() (uint256)`, use the default. (bool success, uint8 result) = _tryGetAssetDecimals(_asset); diff --git a/contracts/internal/EverlongPositions.sol b/contracts/internal/EverlongPositions.sol index 5be325f..8a0740e 100644 --- a/contracts/internal/EverlongPositions.sol +++ b/contracts/internal/EverlongPositions.sol @@ -28,7 +28,7 @@ abstract contract EverlongPositions is EverlongBase, IEverlongPositions { } /// @dev Rebalances the Everlong bond portfolio if needed. - function _rebalance() public override { + function _rebalance() internal override { // Close all mature positions (if present) so that the proceeds can be // used to purchase longs. if (hasMaturedPositions()) { @@ -181,12 +181,12 @@ abstract contract EverlongPositions is EverlongBase, IEverlongPositions { // Obtain the current amount of idle held by Everlong and return if // it is above the target. idle = IERC20(_asset).balanceOf(address(this)); - if (idle > _target) return idle; + if (idle >= _target) return idle; // Close all matured positions and return if updated idle is above // the target. idle += _closeMaturedPositions(); - if (idle > _target) return idle; + if (idle >= _target) return idle; // Close immature positions from oldest to newest until idle is // above the target. @@ -207,7 +207,7 @@ abstract contract EverlongPositions is EverlongBase, IEverlongPositions { _handleCloseLong(uint128(position.bondAmount)); // Return if the updated idle is above the target. - if (idle > _target) return idle; + if (idle >= _target) return idle; positionCount--; } diff --git a/contracts/internal/EverlongStorage.sol b/contracts/internal/EverlongStorage.sol index 9e8a7a6..6a3249e 100644 --- a/contracts/internal/EverlongStorage.sol +++ b/contracts/internal/EverlongStorage.sol @@ -54,7 +54,7 @@ abstract contract EverlongStorage { uint256 internal _virtualAssets; /// @dev Decimals used by the `_asset`. - uint8 internal _decimals; + uint8 internal immutable _decimals; /// @dev Name of the Everlong token. string internal _name; diff --git a/test/units/EverlongAdmin.t.sol b/test/units/EverlongAdmin.t.sol index c120669..1dd4714 100644 --- a/test/units/EverlongAdmin.t.sol +++ b/test/units/EverlongAdmin.t.sol @@ -17,6 +17,7 @@ contract TestEverlongAdmin is EverlongTest { /// @dev Validates successful `setAdmin` call by current `admin`. function test_setAdmin_success_deployer() external { + vm.startPrank(deployer); // Ensure that the deployer can set the admin address. vm.expectEmit(true, true, true, true); emit AdminUpdated(alice); From d49b040f3cf040ce453f78bb06516ce047869d12 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Sun, 25 Aug 2024 21:20:41 -0500 Subject: [PATCH 20/27] variable reordering --- contracts/internal/EverlongStorage.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/internal/EverlongStorage.sol b/contracts/internal/EverlongStorage.sol index 6a3249e..1ab84dd 100644 --- a/contracts/internal/EverlongStorage.sol +++ b/contracts/internal/EverlongStorage.sol @@ -48,14 +48,14 @@ abstract contract EverlongStorage { /// @dev Address of the token to use for Hyperdrive bond purchase/close. address internal immutable _asset; + /// @dev Decimals used by the `_asset`. + uint8 internal immutable _decimals; + // TODO: Remove in favor of more sophisticated position valuation. // TODO: Use some SafeMath library. /// @dev Virtual asset count to track amount deposited into Hyperdrive. uint256 internal _virtualAssets; - /// @dev Decimals used by the `_asset`. - uint8 internal immutable _decimals; - /// @dev Name of the Everlong token. string internal _name; From bf4a2e1604179a9701c04b365fe899fd8034cff8 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Sun, 25 Aug 2024 21:23:36 -0500 Subject: [PATCH 21/27] file casing import fix --- test/units/Everlong.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/units/Everlong.t.sol b/test/units/Everlong.t.sol index b8fe73a..e5c40e8 100644 --- a/test/units/Everlong.t.sol +++ b/test/units/Everlong.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; -import { EVERLONG_KIND, EVERLONG_VERSION } from "../../contracts/libraries/constants.sol"; +import { EVERLONG_KIND, EVERLONG_VERSION } from "../../contracts/libraries/Constants.sol"; import { EverlongTest } from "../harnesses/EverlongTest.sol"; /// @dev Tests Everlong functionality. From 9969f42ab5d74eb75a1429265280ced6b5feecd3 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Tue, 27 Aug 2024 03:12:04 -0500 Subject: [PATCH 22/27] Update contracts/internal/EverlongPositions.sol Co-authored-by: Alex Towle --- contracts/internal/EverlongPositions.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/internal/EverlongPositions.sol b/contracts/internal/EverlongPositions.sol index 8a0740e..0366e46 100644 --- a/contracts/internal/EverlongPositions.sol +++ b/contracts/internal/EverlongPositions.sol @@ -207,7 +207,9 @@ abstract contract EverlongPositions is EverlongBase, IEverlongPositions { _handleCloseLong(uint128(position.bondAmount)); // Return if the updated idle is above the target. - if (idle >= _target) return idle; + if (idle >= _target) { + return idle; + } positionCount--; } From 8d6f178ae9cec82253985200a75b4004b79aeb47 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Tue, 27 Aug 2024 03:12:21 -0500 Subject: [PATCH 23/27] Update contracts/internal/EverlongPositions.sol Co-authored-by: Alex Towle --- contracts/internal/EverlongPositions.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/internal/EverlongPositions.sol b/contracts/internal/EverlongPositions.sol index 0366e46..b4f68b1 100644 --- a/contracts/internal/EverlongPositions.sol +++ b/contracts/internal/EverlongPositions.sol @@ -186,7 +186,9 @@ abstract contract EverlongPositions is EverlongBase, IEverlongPositions { // Close all matured positions and return if updated idle is above // the target. idle += _closeMaturedPositions(); - if (idle >= _target) return idle; + if (idle >= _target) { + return idle; + } // Close immature positions from oldest to newest until idle is // above the target. From 4de9b56792cbe85e3becfbecd0c021714a884143 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Tue, 27 Aug 2024 03:12:29 -0500 Subject: [PATCH 24/27] Update contracts/internal/EverlongPositions.sol Co-authored-by: Alex Towle --- contracts/internal/EverlongPositions.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/internal/EverlongPositions.sol b/contracts/internal/EverlongPositions.sol index b4f68b1..eab954c 100644 --- a/contracts/internal/EverlongPositions.sol +++ b/contracts/internal/EverlongPositions.sol @@ -181,7 +181,9 @@ abstract contract EverlongPositions is EverlongBase, IEverlongPositions { // Obtain the current amount of idle held by Everlong and return if // it is above the target. idle = IERC20(_asset).balanceOf(address(this)); - if (idle >= _target) return idle; + if (idle >= _target) { + return idle; + } // Close all matured positions and return if updated idle is above // the target. From 84bbb0c5a0ff83407651e08351e210fddffa3d04 Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Tue, 27 Aug 2024 03:12:44 -0500 Subject: [PATCH 25/27] Update test/exposed/EverlongExposed.sol Co-authored-by: Alex Towle --- test/exposed/EverlongExposed.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/test/exposed/EverlongExposed.sol b/test/exposed/EverlongExposed.sol index bd8d02e..3561059 100644 --- a/test/exposed/EverlongExposed.sol +++ b/test/exposed/EverlongExposed.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.20; import { Test } from "forge-std/Test.sol"; import { Everlong } from "../../contracts/Everlong.sol"; - import { EverlongAdminExposed } from "./EverlongAdminExposed.sol"; import { EverlongERC4626Exposed } from "./EverlongERC4626Exposed.sol"; import { EverlongPositionsExposed } from "./EverlongPositionsExposed.sol"; From 41326d121c50b6c3af0014b5414edcbc1917205c Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Tue, 27 Aug 2024 03:13:01 -0500 Subject: [PATCH 26/27] Update test/exposed/EverlongERC4626Exposed.sol Co-authored-by: Alex Towle --- test/exposed/EverlongERC4626Exposed.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/test/exposed/EverlongERC4626Exposed.sol b/test/exposed/EverlongERC4626Exposed.sol index 39b95cb..b1945cd 100644 --- a/test/exposed/EverlongERC4626Exposed.sol +++ b/test/exposed/EverlongERC4626Exposed.sol @@ -9,6 +9,7 @@ abstract contract EverlongERC4626Exposed is EverlongERC4626 { function exposed_beforeWithdraw(uint256 _assets, uint256 _shares) public { return _beforeWithdraw(_assets, _shares); } + function exposed_afterDeposit(uint256 _assets, uint256 _shares) public { return _afterDeposit(_assets, _shares); } From 937307dc1ebaa348b92e5024f0a8ba94dcf66f2c Mon Sep 17 00:00:00 2001 From: "John McClure (pickleback)" Date: Tue, 27 Aug 2024 03:16:56 -0500 Subject: [PATCH 27/27] addressing feedback: asBase public function --- contracts/Everlong.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/Everlong.sol b/contracts/Everlong.sol index 0dec7b0..e5d82d6 100644 --- a/contracts/Everlong.sol +++ b/contracts/Everlong.sol @@ -116,8 +116,14 @@ contract Everlong is EverlongAdmin, EverlongPositions, EverlongERC4626 { return EVERLONG_VERSION; } - /// @notice Gets the address of the underlying Hyperdrive Instance + /// @notice Gets the address of the underlying Hyperdrive Instance. function hyperdrive() external view returns (address) { return _hyperdrive; } + + /// @notice Gets whether Everlong uses Hyperdrive's base token to + /// transact. + function asBase() external view returns (bool) { + return _asBase; + } }