Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 29 additions & 21 deletions contracts/EverlongStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
)
) {
Expand Down Expand Up @@ -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 │
// ╰───────────────────────────────────────────────────────────────────────╯
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions contracts/interfaces/IEverlongStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions contracts/libraries/HyperdriveExecution.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
19 changes: 19 additions & 0 deletions test/VaultTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 │
// ╰───────────────────────────────────────────────────────────────────────╯
Expand Down
72 changes: 72 additions & 0 deletions test/everlong/EverlongForkSDAITest.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
78 changes: 78 additions & 0 deletions test/everlong/EverlongForkStETHTest.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
2 changes: 1 addition & 1 deletion test/everlong/EverlongTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading
Loading