diff --git a/contracts/EverlongStrategy.sol b/contracts/EverlongStrategy.sol index 687f874..178de0f 100644 --- a/contracts/EverlongStrategy.sol +++ b/contracts/EverlongStrategy.sol @@ -8,7 +8,7 @@ import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; import { BaseStrategy, ERC20 } from "tokenized-strategy/BaseStrategy.sol"; import { IEverlongStrategy } from "./interfaces/IEverlongStrategy.sol"; import { IERC20Wrappable } from "./interfaces/IERC20Wrappable.sol"; -import { EVERLONG_STRATEGY_KIND, EVERLONG_VERSION, ONE } from "./libraries/Constants.sol"; +import { EVERLONG_STRATEGY_KIND, EVERLONG_VERSION, MAX_BPS, ONE } from "./libraries/Constants.sol"; import { EverlongPortfolioLibrary } from "./libraries/EverlongPortfolio.sol"; import { HyperdriveExecutionLibrary } from "./libraries/HyperdriveExecution.sol"; @@ -103,6 +103,12 @@ contract EverlongStrategy is BaseStrategy { // │ Constants and Immutables │ // ╰───────────────────────────────────────────────────────────────────────╯ + /// @notice Amount to add to hyperdrive's minimum transaction amount to + /// account for hyperdrive's internal rounding. Represented as a + /// percentage of the value after conversions from base to shares + /// (if applicable) where 1e18 represents a 100% buffer. + uint256 public constant minimumTransactionAmountBuffer = 0.001e18; + /// @notice Amount of additional bonds to close during a partial position /// closure to avoid rounding errors. Represented as a percentage /// of the positions total amount of bonds where 1e18 represents @@ -330,7 +336,7 @@ contract EverlongStrategy is BaseStrategy { uint256 toSpend = _totalIdle.min(availableDepositLimit(address(this))); // If Everlong has sufficient idle, open a new position. - if (toSpend > _minimumTransactionAmount()) { + if (toSpend > minimumTransactionAmount()) { (uint256 maturityTime, uint256 bondAmount) = _openLong( toSpend, tendConfig.minOutput, @@ -473,7 +479,7 @@ contract EverlongStrategy is BaseStrategy { uint256 _targetOutput ) internal returns (uint256 output) { // Round `_targetOutput` up to Hyperdrive's minimum transaction amount. - _targetOutput = _targetOutput.max(_minimumTransactionAmount()); + _targetOutput = _targetOutput.max(minimumTransactionAmount()); // Since multiple position's worth of bonds may need to be closed, // iterate through each position starting with the most mature. @@ -502,7 +508,7 @@ contract EverlongStrategy is BaseStrategy { // Hyperdrive's minimum transaction amount. if ( totalPositionValue > - (_targetOutput - output + _minimumTransactionAmount()).mulUp( + (_targetOutput - output + minimumTransactionAmount()).mulUp( ONE + partialPositionClosureBuffer ) ) { @@ -682,22 +688,6 @@ contract EverlongStrategy is BaseStrategy { } } - /// @dev Retrieve hyperdrive's minimum transaction amount. - /// @return amount Hyperdrive's minimum transaction amount. - function _minimumTransactionAmount() - internal - view - returns (uint256 amount) - { - amount = _poolConfig.minimumTransactionAmount; - - // Since `amount` is denominated in hyperdrive's base currency. We must - // convert it. - if (!asBase || isWrapped) { - IHyperdrive(hyperdrive)._convertToShares(amount); - } - } - // ╭───────────────────────────────────────────────────────────────────────╮ // │ Views │ // ╰───────────────────────────────────────────────────────────────────────╯ @@ -752,7 +742,7 @@ contract EverlongStrategy is BaseStrategy { /// @return True if a new position can be opened, false otherwise. function canOpenPosition() public view returns (bool) { uint256 currentBalance = asset.balanceOf(address(this)); - return currentBalance > _minimumTransactionAmount(); + return currentBalance > minimumTransactionAmount(); } /// @notice Converts an amount denominated in wrapped tokens to an amount @@ -793,6 +783,24 @@ contract EverlongStrategy is BaseStrategy { IHyperdrive(hyperdrive).isMature(_portfolio.head()); } + /// @notice Gets the minimum amount of strategy assets needed to open a long + /// with hyperdrive. + /// @return amount Minimum amount of strategy assets needed to open a long + /// with hyperdrive. + function minimumTransactionAmount() public view returns (uint256 amount) { + // Retrieve the minimum transaction amount from the poolConfig. + // This value is already converted to shares if `asBase==false`. + amount = IHyperdrive(hyperdrive)._minimumTransactionAmount( + _poolConfig, + asBase + ); + + // NOTE: Since some rounding occurs within hyperdrive when using its + // shares token, we choose to be safe and add a buffer to ensure + // that the amount will be above hyperdrive's minimum. + amount = amount.mulUp(ONE + minimumTransactionAmountBuffer); + } + /// @notice Retrieve the position at the specified location in the queue. /// @param _index Index in the queue to retrieve the position. /// @return The position at the specified location. diff --git a/contracts/interfaces/IEverlongStrategy.sol b/contracts/interfaces/IEverlongStrategy.sol index 88d154b..2e5ca60 100644 --- a/contracts/interfaces/IEverlongStrategy.sol +++ b/contracts/interfaces/IEverlongStrategy.sol @@ -118,6 +118,18 @@ interface IEverlongStrategy is IPermissionedStrategy, IEverlongEvents { /// @return The Everlong instance's kind. function kind() external pure returns (string memory); + /// @notice Gets the minimum amount of strategy assets needed to open a long + /// with hyperdrive. + /// @return Minimum amount of strategy assets needed to open a long with + /// hyperdrive. + function minimumTransactionAmount() external view returns (uint256); + + /// @notice Amount to add to hyperdrive's minimum transaction amount to + /// account for hyperdrive's internal rounding. Represented as a + /// percentage of the value after conversions from base to shares + /// (if applicable) where 1e18 represents a 100% buffer. + function minimumTransactionAmountBuffer() external view returns (uint256); + /// @notice Amount of additional bonds to close during a partial position /// closure to avoid rounding errors. Represented as a percentage /// of the positions total amount of bonds where 1e18 represents diff --git a/contracts/libraries/HyperdriveExecution.sol b/contracts/libraries/HyperdriveExecution.sol index ce4eb89..8137d8b 100644 --- a/contracts/libraries/HyperdriveExecution.sol +++ b/contracts/libraries/HyperdriveExecution.sol @@ -1432,4 +1432,25 @@ library HyperdriveExecutionLibrary { } return self.convertToShares(_baseAmount); } + + /// @dev Gets the minimum amount of strategy assets needed to open a long + /// with hyperdrive. + /// @param _poolConfig The hyperdrive PoolConfig. + /// @param _asBase Whether to transact in hyperdrive's base token or vault + /// shares token. + /// @return amount Minimum amount of strategy assets needed to open a long + /// with hyperdrive. + function _minimumTransactionAmount( + IHyperdrive self, + IHyperdrive.PoolConfig storage _poolConfig, + bool _asBase + ) public view returns (uint256 amount) { + amount = _poolConfig.minimumTransactionAmount; + + // Since `amount` is denominated in hyperdrive's base token. We must + // convert it to the shares token if `_asBase` is set to false. + if (!_asBase) { + amount = _convertToShares(self, amount); + } + } } diff --git a/test/VaultTest.sol b/test/VaultTest.sol index dbd0e35..3462f80 100644 --- a/test/VaultTest.sol +++ b/test/VaultTest.sol @@ -232,6 +232,25 @@ abstract contract VaultTest is HyperdriveTest { vm.stopPrank(); } + /// @dev Create all the role-based and arbitrary users used in testing. + /// The `setUp` function from `HyperdriveTest` creates these for us, + /// but if its overridden then this must be called. + function createTestUsers() internal { + // Create role-based users. + (deployer, ) = createUser("deployer"); + (governance, ) = createUser("governance"); + (keeper, ) = createUser("keeper"); + (management, ) = createUser("management"); + (emergencyAdmin, ) = createUser("emergencyAdmin"); + + // Create arbitrary test users. + (alice, alicePK) = createUser("alice"); + (bob, bobPK) = createUser("bob"); + (celine, celinePK) = createUser("celine"); + (dan, danPK) = createUser("dan"); + (eve, evePK) = createUser("eve"); + } + // ╭───────────────────────────────────────────────────────────────────────╮ // │ Deposit Helpers │ // ╰───────────────────────────────────────────────────────────────────────╯ diff --git a/test/everlong/EverlongForkSDAITest.sol b/test/everlong/EverlongForkSDAITest.sol new file mode 100644 index 0000000..94c206d --- /dev/null +++ b/test/everlong/EverlongForkSDAITest.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import { console2 as console } from "forge-std/console2.sol"; +import { IERC20, IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; +import { ILido } from "hyperdrive/contracts/src/interfaces/ILido.sol"; +import { IEverlongStrategy } from "../../contracts/interfaces/IEverlongStrategy.sol"; +import { EVERLONG_STRATEGY_KIND, EVERLONG_VERSION } from "../../contracts/libraries/Constants.sol"; +import { EverlongTest } from "./EverlongTest.sol"; + +/// @dev Configures the testing Everlong instance to point to the existing +/// SDAIHyperdrive instance on mainnet. +contract EverlongForkSDAITest is EverlongTest { + address internal SDAI_HYPERDRIVE_ADDRESS = + 0x324395D5d835F84a02A75Aa26814f6fD22F25698; + + address internal SDAI_ADDRESS = 0x83F20F44975D03b1b09e64809B757c47f942BEeA; + + address internal SDAI_WHALE = 0x27d3745135693647155d87706FBFf3EB5B7345c2; + + function setUp() public virtual override { + vm.createSelectFork(vm.rpcUrl("mainnet"), FORK_BLOCK_NUMBER); + + createTestUsers(); + + // Set up the strategy to use the current StETH hyperdrive instance. + hyperdrive = IHyperdrive(SDAI_HYPERDRIVE_ADDRESS); + AS_BASE = false; + IS_WRAPPED = false; + + setUpRoleManager(); + setUpEverlongStrategy(); + setUpEverlongVault(); + } + + /// @dev "Mint" tokens to an account by transferring from the whale. + /// @param _amount Amount of tokens to "mint". + /// @param _to Destination for the tokens. + function mintSDAI(uint256 _amount, address _to) internal { + vm.startPrank(SDAI_WHALE); + asset.transfer(_to, _amount); + vm.stopPrank(); + } + + /// @dev Deposit into the SDAI everlong vault. + /// @param _assets Amount of assets to deposit. + /// @param _from Source of the tokens. + /// @return shares Amount of shares received from the deposit. + function depositSDAI( + uint256 _assets, + address _from + ) internal returns (uint256 shares) { + mintSDAI(_assets, _from); + vm.startPrank(_from); + asset.approve(address(vault), _assets); + shares = vault.deposit(_assets, _from); + vm.stopPrank(); + } + + /// @dev Redeem shares from the SDAI everlong vault. + /// @param _shares Amount of shares to redeem. + /// @param _from Source of the shares. + /// @return assets Amount of assets received from the redemption. + function redeemSDAI( + uint256 _shares, + address _from + ) internal returns (uint256 assets) { + vm.startPrank(_from); + assets = vault.redeem(_shares, _from, _from); + vm.stopPrank(); + } +} diff --git a/test/everlong/EverlongForkStETHTest.sol b/test/everlong/EverlongForkStETHTest.sol new file mode 100644 index 0000000..a27867a --- /dev/null +++ b/test/everlong/EverlongForkStETHTest.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import { console2 as console } from "forge-std/console2.sol"; +import { IERC20, IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; +import { ILido } from "hyperdrive/contracts/src/interfaces/ILido.sol"; +import { IEverlongStrategy } from "../../contracts/interfaces/IEverlongStrategy.sol"; +import { EVERLONG_STRATEGY_KIND, EVERLONG_VERSION } from "../../contracts/libraries/Constants.sol"; +import { EverlongTest } from "./EverlongTest.sol"; + +/// @dev Configures the testing Everlong instance to point to the existing +/// StETHHyperdrive instance on mainnet. +contract EverlongForkStETHTest is EverlongTest { + address internal STETH_HYPERDRIVE_ADDRESS = + 0xd7e470043241C10970953Bd8374ee6238e77D735; + + address internal STETH_ADDRESS = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; + + address internal WSTETH_ADDRESS = + 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; + + address internal STETH_WHALE = 0x51C2cEF9efa48e08557A361B52DB34061c025a1B; + + address internal WSTETH_WHALE = 0x5313b39bf226ced2332C81eB97BB28c6fD50d1a3; + + function setUp() public virtual override { + vm.createSelectFork(vm.rpcUrl("mainnet"), FORK_BLOCK_NUMBER); + + createTestUsers(); + + // Set up the strategy to use the current StETH hyperdrive instance. + hyperdrive = IHyperdrive(STETH_HYPERDRIVE_ADDRESS); + AS_BASE = false; + IS_WRAPPED = true; + WRAPPED_ASSET = WSTETH_ADDRESS; + + setUpRoleManager(); + setUpEverlongStrategy(); + setUpEverlongVault(); + } + + /// @dev "Mint" tokens to an account by transferring from the whale. + /// @param _amount Amount of tokens to "mint". + /// @param _to Destination for the tokens. + function mintWSTETH(uint256 _amount, address _to) internal { + vm.startPrank(WSTETH_WHALE); + asset.transfer(_to, _amount); + vm.stopPrank(); + } + + /// @dev Deposit into the WSTETH everlong vault. + /// @param _assets Amount of assets to deposit. + /// @param _from Source of the tokens. + /// @return shares Amount of shares received from the deposit. + function depositWSTETH( + uint256 _assets, + address _from + ) internal returns (uint256 shares) { + mintWSTETH(_assets, _from); + vm.startPrank(_from); + asset.approve(address(vault), _assets); + shares = vault.deposit(_assets, _from); + vm.stopPrank(); + } + + /// @dev Redeem shares from the WSTETH everlong vault. + /// @param _shares Amount of shares to redeem. + /// @param _from Source of the shares. + /// @return assets Amount of assets received from the redemption. + function redeemWSTETH( + uint256 _shares, + address _from + ) internal returns (uint256 assets) { + vm.startPrank(_from); + assets = vault.redeem(_shares, _from, _from); + vm.stopPrank(); + } +} diff --git a/test/everlong/EverlongTest.sol b/test/everlong/EverlongTest.sol index 8b3c030..b09fda9 100644 --- a/test/everlong/EverlongTest.sol +++ b/test/everlong/EverlongTest.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import { console2 as console } from "forge-std/console2.sol"; -import { IERC20 } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; +import { IERC20, IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol"; import { ERC20Mintable } from "hyperdrive/contracts/test/ERC20Mintable.sol"; import { HyperdriveTest } from "hyperdrive/test/utils/HyperdriveTest.sol"; diff --git a/test/everlong/integration/SDAIVaultSharesToken.t.sol b/test/everlong/integration/SDAIVaultSharesToken.t.sol deleted file mode 100644 index 999e49b..0000000 --- a/test/everlong/integration/SDAIVaultSharesToken.t.sol +++ /dev/null @@ -1,191 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.20; - -import { console2 as console } from "forge-std/console2.sol"; -import { IERC20, IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; -import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol"; -import { Lib } from "hyperdrive/test/utils/Lib.sol"; -import { IVault } from "yearn-vaults-v3/interfaces/IVault.sol"; -import { IPermissionedStrategy } from "../../../contracts/interfaces/IPermissionedStrategy.sol"; -import { MAX_BPS } from "../../../contracts/libraries/Constants.sol"; -import { HyperdriveExecutionLibrary } from "../../../contracts/libraries/HyperdriveExecution.sol"; -import { EverlongStrategy } from "../../../contracts/EverlongStrategy.sol"; -import { EverlongTest } from "../EverlongTest.sol"; - -/// @dev Test ensuring that Everlong works with sDAI Hyperdrive and -/// AS_BASE=false. -contract TestSDAIVaultSharesToken is EverlongTest { - using FixedPointMath for *; - using Lib for *; - using HyperdriveExecutionLibrary for *; - - /// @dev SDAI whale account used for easy token minting. - address WHALE = 0x0740c011A4160139Bd2E4EA091581d35ee3454da; - - /// @dev "Mint" tokens to an account by transferring from the whale. - /// @param _amount Amount of tokens to "mint". - /// @param _to Destination for the tokens. - function mintAsset(uint256 _amount, address _to) internal { - vm.startPrank(WHALE); - asset.transfer(_to, _amount); - vm.stopPrank(); - } - - /// @dev Deposit into the SDAI everlong vault. - /// @param _assets Amount of assets to deposit. - /// @param _from Source of the tokens. - /// @return shares Amount of shares received from the deposit. - function depositSDAI( - uint256 _assets, - address _from - ) internal returns (uint256 shares) { - mintAsset(_assets, _from); - vm.startPrank(_from); - asset.approve(address(vault), _assets); - shares = vault.deposit(_assets, _from); - vm.stopPrank(); - } - - /// @dev Redeem shares from the SDAI everlong vault. - /// @param _shares Amount of shares to redeem. - /// @param _from Source of the shares. - /// @return assets Amount of assets received from the redemption. - function redeemSDAI( - uint256 _shares, - address _from - ) internal returns (uint256 assets) { - vm.startPrank(_from); - assets = vault.redeem(_shares, _from, _from); - vm.stopPrank(); - } - - /// @dev Deploy a strategy pointing to the sDAI hyperdrive instance and - /// create a vault around it. - function setUp() public virtual override { - super.setUp(); - - // sDai Hyperdrive mainnet address. - hyperdrive = IHyperdrive(0x324395D5d835F84a02A75Aa26814f6fD22F25698); - - // Set the correct asset. - asset = IERC20(hyperdrive.vaultSharesToken()); - - vm.startPrank(deployer); - - // Deploy and configure the strategy. - strategy = IPermissionedStrategy( - address( - new EverlongStrategy( - address(asset), - "sDAI Strategy", - address(hyperdrive), - false, - false - ) - ) - ); - strategy.setPerformanceFeeRecipient(governance); - strategy.setKeeper(address(keeperContract)); - strategy.setPendingManagement(management); - strategy.setEmergencyAdmin(emergencyAdmin); - - // Issue the deployer a bunch of stETH... this makes it easy to dish - // out to other users later. - // uint256 deployerETH = 1_000e18; - // deal(deployer, deployerETH); - // ILido(address(asset)).submit{ value: deployerETH }(deployer); - - vm.stopPrank(); - - // As the `management` address: - // 1. Accept the `management` role for the strategy. - // 2. Set the `profitMaxUnlockTime` to zero. - vm.startPrank(management); - strategy.acceptManagement(); - strategy.setProfitMaxUnlockTime(STRATEGY_PROFIT_MAX_UNLOCK_TIME); - strategy.setPerformanceFee(0); - vm.stopPrank(); - - // As the `governance` address: - // 1. Deploy the Vault using the RoleManager. - // 2. Add the EverlongStrategy to the vault. - // 3. Update the max debt for the strategy to be the maximum uint256. - // 4. Configure the vault to `auto_allocate` which will automatically - // update the strategy's debt on deposit. - vm.startPrank(governance); - vault = IVault( - roleManager.newVault( - address(asset), - 0, - EVERLONG_NAME, - EVERLONG_SYMBOL - ) - ); - vault.add_strategy(address(strategy)); - vault.update_max_debt_for_strategy( - address(strategy), - type(uint256).max - ); - roleManager.setPositionHolder( - roleManager.KEEPER(), - address(keeperContract) - ); - vm.stopPrank(); - - // As the `management` address, configure the DebtAllocator to not - // wait to update a strategy's debt and set the minimum change before - // updating to just above hyperdrive's minimum transaction amount. - vm.startPrank(management); - // Set the vault's duration for unlocking profit. - vault.setProfitMaxUnlockTime(VAULT_PROFIT_MAX_UNLOCK_TIME); - // Enable deposits to the strategy from the vault. - strategy.setDepositor(address(vault), true); - // Give the `EverlongStrategyKeeper` role to the keeper address. - debtAllocator.setKeeper(address(keeperContract), true); - // Set minimum wait time for updating strategy debt. - debtAllocator.setMinimumWait(0); - // Set minimum change in debt for triggering an update. - debtAllocator.setMinimumChange( - address(vault), - MINIMUM_TRANSACTION_AMOUNT + 1 - ); - debtAllocator.setStrategyDebtRatio( - address(vault), - address(strategy), - MAX_BPS - TARGET_IDLE_LIQUIDITY_BASIS_POINTS, - MAX_BPS - MIN_IDLE_LIQUIDITY_BASIS_POINTS - ); - vm.stopPrank(); - } - - /// @dev Ensure the deposit functions work as expected. - function test_deposit() external { - // Alice and Bob deposit into the vault. - uint256 depositAmount = 100e18; - uint256 aliceShares = depositSDAI(depositAmount, alice); - uint256 bobShares = depositSDAI(depositAmount, bob); - - // Alice and Bob should have non-zero share amounts. - assertGt(aliceShares, 0); - assertGt(bobShares, 0); - } - - /// @dev Ensure the rebalance and redeem functions work as expected. - function test_redeem() external { - // Alice and Bob deposit into the vault. - uint256 depositAmount = 100e18; - uint256 aliceShares = depositSDAI(depositAmount, alice); - uint256 bobShares = depositSDAI(depositAmount, bob); - - // The vault allocates funds to the strategy. - rebalance(); - - // Alice and Bob redeem their shares from the vault. - uint256 aliceRedeemAssets = redeemSDAI(aliceShares, alice); - uint256 bobRedeemAssets = redeemSDAI(bobShares, bob); - - // Neither Alice nor Bob should have more assets than they began with. - assertLe(aliceRedeemAssets, depositAmount); - assertLe(bobRedeemAssets, depositAmount); - } -} diff --git a/test/everlong/units/Everlong.t.sol b/test/everlong/units/Everlong.t.sol index 8749c88..a4accc1 100644 --- a/test/everlong/units/Everlong.t.sol +++ b/test/everlong/units/Everlong.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; +import { IERC20, IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; +import { ILido } from "hyperdrive/contracts/src/interfaces/ILido.sol"; import { IEverlongStrategy } from "../../../contracts/interfaces/IEverlongStrategy.sol"; import { EVERLONG_STRATEGY_KIND, EVERLONG_VERSION } from "../../../contracts/libraries/Constants.sol"; import { EverlongTest } from "../EverlongTest.sol"; @@ -25,6 +27,34 @@ contract TestEverlong is EverlongTest { ); } + function test_minimumTransactionAmount_asBase_true() external { + // Obtain the minimum transaction amount from the strategy. + uint256 minTxAmount = IEverlongStrategy(address(strategy)) + .minimumTransactionAmount(); + + // Open a long with that amount and `asBase` set to true (the default + // for testing). + (uint256 maturityTime, uint256 bondAmount) = openLong( + alice, + minTxAmount, + AS_BASE + ); + + // Ensure the maturityTime and bondAmount are valid. + assertGt(maturityTime, 0); + assertGt(bondAmount, 0); + } + + /// @dev Ensure that the `minimumTransactionAmountBuffer` view function is + /// implemented. + function test_minimumTransactionAmountBuffer() external view { + assertGt( + IEverlongStrategy(address(strategy)) + .minimumTransactionAmountBuffer(), + 0 + ); + } + /// @dev Ensure that the `version()` view function is implemented. function test_version() external view { assertEq( diff --git a/test/everlong/units/EverlongForkSDAITest.t.sol b/test/everlong/units/EverlongForkSDAITest.t.sol new file mode 100644 index 0000000..63261e4 --- /dev/null +++ b/test/everlong/units/EverlongForkSDAITest.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import { console2 as console } from "forge-std/console2.sol"; +import { IERC20, IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; +import { ILido } from "hyperdrive/contracts/src/interfaces/ILido.sol"; +import { IEverlongStrategy } from "../../../contracts/interfaces/IEverlongStrategy.sol"; +import { EVERLONG_STRATEGY_KIND, EVERLONG_VERSION } from "../../../contracts/libraries/Constants.sol"; +import { EverlongForkSDAITest } from "../EverlongForkSDAITest.sol"; + +/// @dev Tests Everlong functionality when using the existing SDAIHyperdrive +/// instance on a fork. +contract TestEverlongForkSDAI is EverlongForkSDAITest { + /// @dev Ensure the deposit functions work as expected. + function test_deposit() external { + // Alice and Bob deposit into the vault. + uint256 depositAmount = 100e18; + uint256 aliceShares = depositSDAI(depositAmount, alice); + uint256 bobShares = depositSDAI(depositAmount, bob); + + // Alice and Bob should have non-zero share amounts. + assertGt(aliceShares, 0); + assertGt(bobShares, 0); + } + + /// @dev Ensure the rebalance and redeem functions work as expected. + function test_redeem() external { + // Alice and Bob deposit into the vault. + uint256 depositAmount = 100e18; + uint256 aliceShares = depositSDAI(depositAmount, alice); + uint256 bobShares = depositSDAI(depositAmount, bob); + + // The vault allocates funds to the strategy. + rebalance(); + + // Alice and Bob redeem their shares from the vault. + uint256 aliceRedeemAssets = redeemSDAI(aliceShares, alice); + uint256 bobRedeemAssets = redeemSDAI(bobShares, bob); + + // Neither Alice nor Bob should have more assets than they began with. + assertLe(aliceRedeemAssets, depositAmount); + assertLe(bobRedeemAssets, depositAmount); + } + + /// @dev Ensure that the `minimumTransactionAmount` calculated by the + /// strategy is greater than or equal to hyperdrive's representation. + function test_minimumTransactionAmount_asBase_false_not_wrapped() external { + // Obtain the minimum transaction amount from the strategy. + uint256 minTxAmount = IEverlongStrategy(address(strategy)) + .minimumTransactionAmount(); + + // Manually mint alice plenty of hyperdrive vault shares tokens. + mintSDAI(minTxAmount * 100, alice); + vm.prank(alice); + IERC20(SDAI_ADDRESS).approve(address(hyperdrive), type(uint256).max); + + vm.startPrank(alice); + // Open a long in hyperdrive with the vault shares tokens. + (uint256 maturityTime, uint256 bondAmount) = hyperdrive.openLong( + minTxAmount, + 0, // min bond proceeds + 0, // min vault share price + IHyperdrive.Options({ + destination: alice, + asBase: false, + extraData: "" + }) + ); + vm.stopPrank(); + + // Ensure the maturityTime and bondAmount are valid. + assertGt(maturityTime, 0); + assertGt(bondAmount, 0); + } +} diff --git a/test/everlong/units/EverlongForkStETHTest.t.sol b/test/everlong/units/EverlongForkStETHTest.t.sol new file mode 100644 index 0000000..339f26d --- /dev/null +++ b/test/everlong/units/EverlongForkStETHTest.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import { IERC20, IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol"; +import { ILido } from "hyperdrive/contracts/src/interfaces/ILido.sol"; +import { IEverlongStrategy } from "../../../contracts/interfaces/IEverlongStrategy.sol"; +import { EVERLONG_STRATEGY_KIND, EVERLONG_VERSION } from "../../../contracts/libraries/Constants.sol"; +import { EverlongForkStETHTest } from "../EverlongForkStETHTest.sol"; + +/// @dev Tests Everlong functionality when using the existing StETHHyperdrive +/// instance on a fork. +contract TestEverlongForkStETH is EverlongForkStETHTest { + /// @dev Ensure the deposit functions work as expected. + function test_deposit() external { + // Alice and Bob deposit into the vault. + uint256 depositAmount = 100e18; + uint256 aliceShares = depositWSTETH(depositAmount, alice); + uint256 bobShares = depositWSTETH(depositAmount, bob); + + // Alice and Bob should have non-zero share amounts. + assertGt(aliceShares, 0); + assertGt(bobShares, 0); + } + + /// @dev Ensure the rebalance and redeem functions work as expected. + function test_redeem() external { + // Alice and Bob deposit into the vault. + uint256 depositAmount = 100e18; + uint256 aliceShares = depositWSTETH(depositAmount, alice); + uint256 bobShares = depositWSTETH(depositAmount, bob); + + // The vault allocates funds to the strategy. + rebalance(); + + // Alice and Bob redeem their shares from the vault. + uint256 aliceRedeemAssets = redeemWSTETH(aliceShares, alice); + uint256 bobRedeemAssets = redeemWSTETH(bobShares, bob); + + // Neither Alice nor Bob should have more assets than they began with. + assertLe(aliceRedeemAssets, depositAmount); + assertLe(bobRedeemAssets, depositAmount); + } + + /// @dev Ensure that the `minimumTransactionAmount` calculated by the + /// strategy is greater than or equal to hyperdrive's representation. + function test_minimumTransactionAmount_asBase_false_wrapped() external { + // Obtain the minimum transaction amount from the strategy. + uint256 minTxAmount = IEverlongStrategy(address(strategy)) + .minimumTransactionAmount(); + + // Manually mint alice plenty of hyperdrive vault shares tokens. + vm.startPrank(STETH_WHALE); + ILido(STETH_ADDRESS).transferShares(alice, minTxAmount * 100); + ILido(STETH_ADDRESS).approve(address(hyperdrive), type(uint256).max); + + // Open a long in hyperdrive with the vault shares tokens. + (uint256 maturityTime, uint256 bondAmount) = hyperdrive.openLong( + minTxAmount, + 0, // min bond proceeds + 0, // min vault share price + IHyperdrive.Options({ + destination: alice, + asBase: false, + extraData: "" + }) + ); + + // Ensure the maturityTime and bondAmount are valid. + assertGt(maturityTime, 0); + assertGt(bondAmount, 0); + } +} diff --git a/test/everlong/units/Tend.t.sol b/test/everlong/units/Tend.t.sol index 175bf77..af01928 100644 --- a/test/everlong/units/Tend.t.sol +++ b/test/everlong/units/Tend.t.sol @@ -55,8 +55,16 @@ contract TestTend is EverlongTest { uint256 shares; uint256 spent; for (uint256 i; i < maxPositionCount; i++) { - spent += MINIMUM_TRANSACTION_AMOUNT * 2; - shares += depositVault(MINIMUM_TRANSACTION_AMOUNT * 2, alice, true); + spent += + IEverlongStrategy(address(strategy)) + .minimumTransactionAmount() * + 2; + shares += depositVault( + IEverlongStrategy(address(strategy)) + .minimumTransactionAmount() * 2, + alice, + true + ); advanceTimeWithCheckpointsAndReporting(CHECKPOINT_DURATION); } @@ -114,10 +122,13 @@ contract TestTend is EverlongTest { // Mint some tokens to Everlong for opening longs. // Ensure Everlong's balance is gte Hyperdrive's minTransactionAmount. // Ensure `tendTrigger()` returns true. - depositStrategy(MINIMUM_TRANSACTION_AMOUNT + 1, alice); + depositStrategy( + IEverlongStrategy(address(strategy)).minimumTransactionAmount() + 1, + alice + ); assertGt( IERC20(strategy.asset()).balanceOf(address(strategy)), - MINIMUM_TRANSACTION_AMOUNT + IEverlongStrategy(address(strategy)).minimumTransactionAmount() ); (bool canTend, ) = strategy.tendTrigger(); assertTrue( @@ -130,7 +141,10 @@ contract TestTend is EverlongTest { /// position. function test_tendTrigger_with_matured_position() external { // Mint some tokens to Everlong for opening longs and rebalance. - mintApproveAsset(address(strategy), MINIMUM_TRANSACTION_AMOUNT + 1); + mintApproveAsset( + address(strategy), + IEverlongStrategy(address(strategy)).minimumTransactionAmount() + 1 + ); rebalance(); // Increase block.timestamp until position is mature. @@ -202,7 +216,10 @@ contract TestTend is EverlongTest { /// @dev Tests that `TendConfig.minOutput` is obeyed when opening longs. function test_minOutput_open_long() external { // Deposit into the strategy as alice. - depositStrategy(MINIMUM_TRANSACTION_AMOUNT + 1, alice); + depositStrategy( + IEverlongStrategy(address(strategy)).minimumTransactionAmount() + 1, + alice + ); // Start a prank as a keeper. vm.startPrank(keeper); @@ -229,7 +246,10 @@ contract TestTend is EverlongTest { /// @dev Tests that `TendConfig.minVaultSharePrice` is obeyed when opening longs. function test_minVaultSharePrice_open_long() external { // Deposit into the strategy as alice. - depositStrategy(MINIMUM_TRANSACTION_AMOUNT + 1, alice); + depositStrategy( + IEverlongStrategy(address(strategy)).minimumTransactionAmount() + 1, + alice + ); // Start a prank as a keeper. vm.startPrank(keeper); @@ -257,7 +277,10 @@ contract TestTend is EverlongTest { /// tend(). function test_positionClosureLimit_tend() external { // Deposit into the strategy as alice. - depositStrategy(MINIMUM_TRANSACTION_AMOUNT + 1, alice); + depositStrategy( + IEverlongStrategy(address(strategy)).minimumTransactionAmount() + 1, + alice + ); // Rebalance to have the strategy hold a single position. rebalance(); @@ -265,7 +288,10 @@ contract TestTend is EverlongTest { // Fast forward a checkpoint duration, deposit, and rebalance. // This will result in two total positions held by the strategy. advanceTimeWithCheckpoints(CHECKPOINT_DURATION); - depositStrategy(MINIMUM_TRANSACTION_AMOUNT + 1, alice); + depositStrategy( + IEverlongStrategy(address(strategy)).minimumTransactionAmount() + 1, + alice + ); rebalance(); assertEq(IEverlongStrategy(address(strategy)).positionCount(), 2);