Skip to content

Commit

Permalink
vault shares token testing + misc (#33)
Browse files Browse the repository at this point in the history
* add test using hyperdrive's vaultSharesToken via asBase=false

* doc fixes

* use feeless hyperdrive instances for `calculateMaxLong` tests and remove initial time advancement after deploying
  • Loading branch information
mcclurejt authored Dec 12, 2024
1 parent 7803518 commit daabc00
Show file tree
Hide file tree
Showing 10 changed files with 358 additions and 52 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ source .env && \
STRATEGY_NAME='<name_of_a_deployed_strategy>' \
NAME='<your_vault>' \
SYMBOL='<your_vault_symbol>' \
CATEGORY=<your_category_number> \
forge script script/DeployVault.s.sol --rpc-url 0.0.0.0:8545 --broadcast
```

Expand Down
10 changes: 5 additions & 5 deletions contracts/EverlongStrategyKeeper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,22 @@ contract EverlongStrategyKeeper is Ownable {
using SafeERC20 for ERC20;

/// @notice Kind of the EverlongStrategyKeeper.
string constant kind = EVERLONG_STRATEGY_KEEPER_KIND;
string public constant kind = EVERLONG_STRATEGY_KEEPER_KIND;

/// @notice Version of the EverlongStrategyKeeper.
string constant version = EVERLONG_VERSION;
string public constant version = EVERLONG_VERSION;

/// @notice Name of the EverlongStrategyKeeper.
string name;
string public name;

/// @notice Address of the target RoleManager contract.
/// @dev Helpful for getting periphery contract addresses and enumerating
/// vaults.
address roleManager;
address public roleManager;

/// @notice Address of the external `CommonReportTrigger` contract.
/// @dev This contract contains default checks for whether to report+tend.
address trigger;
address public trigger;

/// @notice Initialize the EverlongStrategyKeeper contract.
/// @param _name Name for the keeper contract.
Expand Down
18 changes: 18 additions & 0 deletions contracts/libraries/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ uint256 constant ONE = 1e18;
/// @dev Maximum basis points value (10_000 == 100%).
uint256 constant MAX_BPS = 10_000;

/// @dev We can assume that almost all Hyperdrive deployments have the
/// `convertToBase` and `convertToShares` functions, but there is
/// one legacy sDAI pool that was deployed before these functions
/// were written. We explicitly special case conversions for this
/// pool.
address constant LEGACY_SDAI_HYPERDRIVE = address(
0x324395D5d835F84a02A75Aa26814f6fD22F25698
);

/// @dev We can assume that almost all Hyperdrive deployments have the
/// `convertToBase` and `convertToShares` functions, but there is
/// one legacy stETH pool that was deployed before these functions
/// were written. We explicitly special case conversions for this
/// pool.
address constant LEGACY_STETH_HYPERDRIVE = address(
0xd7e470043241C10970953Bd8374ee6238e77D735
);

/// @dev Yearn RoleManagerFactory address for mainnet, base, and arbitrum.
address constant ROLE_MANAGER_FACTORY_ADDRESS = 0xca12459a931643BF28388c67639b3F352fe9e5Ce;

Expand Down
95 changes: 88 additions & 7 deletions contracts/libraries/HyperdriveExecution.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.20;

import { IERC4626 } from "hyperdrive/contracts/src/interfaces/IERC4626.sol";
import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol";
import { ILido } from "hyperdrive/contracts/src/interfaces/ILido.sol";
import { FixedPointMath } from "hyperdrive/contracts/src/libraries/FixedPointMath.sol";
import { HyperdriveMath } from "hyperdrive/contracts/src/libraries/HyperdriveMath.sol";
import { SafeCast } from "hyperdrive/contracts/src/libraries/SafeCast.sol";
Expand All @@ -11,7 +13,7 @@ import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
import { Packing } from "openzeppelin/utils/Packing.sol";
import { IEverlongEvents } from "../interfaces/IEverlongEvents.sol";
import { IEverlongStrategy } from "../interfaces/IEverlongStrategy.sol";
import { ONE } from "./Constants.sol";
import { ONE, LEGACY_SDAI_HYPERDRIVE, LEGACY_STETH_HYPERDRIVE } from "./Constants.sol";

// TODO: Extract into its own library.
uint256 constant HYPERDRIVE_SHARE_RESERVES_BOND_RESERVES_SLOT = 2;
Expand Down Expand Up @@ -51,7 +53,11 @@ library HyperdriveExecutionLibrary {
_amount,
0,
0,
IHyperdrive.Options(address(this), _asBase, _extraData)
IHyperdrive.Options({
destination: address(this),
asBase: _asBase,
extraData: _extraData
})
);
emit IEverlongEvents.PositionOpened(
maturityTime.toUint128(),
Expand Down Expand Up @@ -79,7 +85,11 @@ library HyperdriveExecutionLibrary {
_amount,
_minOutput,
_minVaultSharePrice,
IHyperdrive.Options(address(this), _asBase, _extraData)
IHyperdrive.Options({
destination: address(this),
asBase: _asBase,
extraData: _extraData
})
);
emit IEverlongEvents.PositionOpened(
maturityTime.toUint128(),
Expand All @@ -103,7 +113,7 @@ library HyperdriveExecutionLibrary {
_calculateOpenLong(
self,
_poolConfig,
_asBase ? self.convertToShares(_amount) : _amount
_asBase ? _convertToShares(self, _amount) : _amount
);
}

Expand Down Expand Up @@ -302,7 +312,7 @@ library HyperdriveExecutionLibrary {
_position
);
if (_asBase) {
return self.convertToBase(shareProceeds);
return _convertToBase(self, shareProceeds);
}
return shareProceeds;
}
Expand Down Expand Up @@ -414,7 +424,8 @@ library HyperdriveExecutionLibrary {
// Correct for any error that crept into the calculation of the share
// amount by converting the shares to base and then back to shares
// using the vault's share conversion logic.
data.shareProceeds = self.convertToShares(
data.shareProceeds = _convertToShares(
self,
data.shareProceeds.mulDown(data.closeVaultSharePrice)
);

Expand Down Expand Up @@ -1232,7 +1243,7 @@ library HyperdriveExecutionLibrary {
/// @dev Obtains the vaultSharePrice from the hyperdrive instance.
/// @return The current vaultSharePrice.
function vaultSharePrice(IHyperdrive self) internal view returns (uint256) {
return self.convertToBase(ONE);
return _convertToBase(self, ONE);
}

/// @dev Returns whether a position is mature.
Expand Down Expand Up @@ -1337,4 +1348,74 @@ library HyperdriveExecutionLibrary {
) internal view returns (IHyperdrive.Checkpoint memory) {
return self.getCheckpoint(getCheckpointIdUp(self, _timestamp));
}

/// @dev Convert the input `_shareAmount` to base assets and return the
/// amount.
/// @dev Modern hyperdrive instances expose a `convertToBase` function but
/// some legacy instances do not. For those legacy cases, we perform
/// the calculation here.
/// @param _shareAmount Amount of shares to convert to base assets.
/// @return The converted base amount.
function _convertToBase(
IHyperdrive self,
uint256 _shareAmount
) internal view returns (uint256) {
// Check whether the chain is mainnet. If so, special accomodations may
// be needed for legacy hyperdrive instances.
if (block.chainid == 1) {
// If the address is the legacy stETH pool, we have to convert the
// proceeds to base manually using Lido's `getPooledEthByShares`
// function.
if (address(self) == LEGACY_STETH_HYPERDRIVE) {
return
ILido(address(self.vaultSharesToken()))
.getPooledEthByShares(_shareAmount);
}
// If the address is the legacy sDAI pool, we have to convert the
// proceeds to base manually using ERC4626's `convertToAssets`
// function.
else if (address(self) == LEGACY_SDAI_HYPERDRIVE) {
return
IERC4626(self.vaultSharesToken()).convertToAssets(
_shareAmount
);
}
}
return self.convertToBase(_shareAmount);
}

/// @dev Convert the input `_baseAmount` to vault shares and return the
/// amount.
/// @dev Modern hyperdrive instances expose a `convertToShares` function but
/// some legacy instances do not. For those legacy cases, we perform
/// the calculation here.
/// @param _baseAmount Amount of base to convert to shares.
/// @return The converted share amount.
function _convertToShares(
IHyperdrive self,
uint256 _baseAmount
) internal view returns (uint256) {
// Check whether the chain is mainnet. If so, special accomodations may
// be needed for legacy hyperdrive instances.
if (block.chainid == 1) {
// If the address is the legacy stETH pool, we have to convert the
// proceeds to shares manually using Lido's `getSharesByPooledEth`
// function.
if (address(self) == LEGACY_STETH_HYPERDRIVE) {
return
ILido(address(self.vaultSharesToken()))
.getSharesByPooledEth(_baseAmount);
}
// If the address is the legacy sDAI pool, we have to convert the
// proceeds to base manually using ERC4626's `convertToShares`
// function.
else if (address(self) == LEGACY_SDAI_HYPERDRIVE) {
return
IERC4626(self.vaultSharesToken()).convertToShares(
_baseAmount
);
}
}
return self.convertToShares(_baseAmount);
}
}
11 changes: 10 additions & 1 deletion script/DeployVault.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ contract DeployVault is BaseDeployScript {
// ╭───────────────────────────────────────────────────────────────────────╮
// │ Optional Arguments │
// ╰───────────────────────────────────────────────────────────────────────╯

/// @dev Category for the vault.
/// @dev Yearn docs state that vaults with a category of 0 are arbitrary,
/// however a category of 1 indicates lowest risk. Whatever method we
/// choose, let's try to be consistent.
uint256 internal CATEGORY;
uint256 internal CATEGORY_DEFAULT = 0;

/// @dev ProfitMaxUnlock for the vault. Should be the same interval that the
/// underlying yield source accrues on.
uint256 internal PROFIT_MAX_UNLOCK;
Expand Down Expand Up @@ -120,6 +128,7 @@ contract DeployVault is BaseDeployScript {
: "";

// Read optional arguments.
CATEGORY = vm.envOr("CATEGORY", CATEGORY_DEFAULT);
PROFIT_MAX_UNLOCK = vm.envOr(
"PROFIT_MAX_UNLOCK",
PROFIT_MAX_UNLOCK_DEFAULT
Expand Down Expand Up @@ -172,7 +181,7 @@ contract DeployVault is BaseDeployScript {
IVault vault = IVault(
IRoleManager(roleManagerAddress).newVault(
IStrategy(strategyAddress).asset(),
0,
CATEGORY,
output.name,
output.symbol
)
Expand Down
36 changes: 28 additions & 8 deletions test/VaultTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ abstract contract VaultTest is HyperdriveTest {
using HyperdriveUtils for *;
using FixedPointMath for *;

// ╭───────────────────────────────────────────────────────────────────────╮
// │ Fork Configuration │
// ╰───────────────────────────────────────────────────────────────────────╯
uint256 FORK_BLOCK_NUMBER = 21_381_521;

// ╭───────────────────────────────────────────────────────────────────────╮
// │ HyperdriveTest Storage │
// ╰───────────────────────────────────────────────────────────────────────╯
Expand Down Expand Up @@ -161,7 +166,7 @@ abstract contract VaultTest is HyperdriveTest {

/// @dev Set up the testing environment on a fork of mainnet.
function setUp() public virtual override {
vm.createSelectFork(vm.rpcUrl("mainnet"));
vm.createSelectFork(vm.rpcUrl("mainnet"), FORK_BLOCK_NUMBER);
super.setUp();
setUpHyperdrive();
setUpRoleManager();
Expand All @@ -181,14 +186,12 @@ abstract contract VaultTest is HyperdriveTest {
GOVERNANCE_LP_FEE,
GOVERNANCE_ZOMBIE_FEE
);
asset = IERC20(hyperdrive.baseToken());

// Seed liquidity for the hyperdrive instance.
if (HYPERDRIVE_INITIALIZER == address(0)) {
HYPERDRIVE_INITIALIZER = deployer;
}
initialize(HYPERDRIVE_INITIALIZER, FIXED_RATE, INITIAL_CONTRIBUTION);
advanceTimeWithCheckpoints(1);

vm.stopPrank();
}
Expand All @@ -207,6 +210,26 @@ abstract contract VaultTest is HyperdriveTest {
debtAllocator = DebtAllocator(roleManager.getDebtAllocator());
accountant = IAccountant(roleManager.getAccountant());
vm.stopPrank();

// As the `governance` address:
// 1. Accept the "Fee Manager" role for the Accountant.
// 2. Set the default `config.maxLoss` for the accountant to be 10%.
// This will enable losses of up to 10% across reports before
// reverting.
vm.startPrank(governance);
accountant.acceptFeeManager();
IAccountant.Fee memory defaultConfig = accountant.defaultConfig();
// Must increase the accountant maxLoss for reporting since `totalAssets`
// decreases whenever opening longs.
accountant.updateDefaultConfig(
0,
0,
0,
0,
defaultConfig.maxGain,
defaultConfig.maxLoss
);
vm.stopPrank();
}

// ╭───────────────────────────────────────────────────────────────────────╮
Expand Down Expand Up @@ -372,14 +395,11 @@ abstract contract VaultTest is HyperdriveTest {
}
}

/// @dev Mint base token to the provided address and approve the vault and
/// @dev Mint token to the provided address and approve the vault and
/// strategy.
/// @param _recipient Receiver of the minted assets.
/// @param _amount Amount of assets to mint.
function mintApproveBaseAsset(
address _recipient,
uint256 _amount
) internal {
function mintApproveAsset(address _recipient, uint256 _amount) internal {
ERC20Mintable(address(asset)).mint(_recipient, _amount);
vm.startPrank(_recipient);
ERC20Mintable(address(asset)).approve(address(vault), _amount);
Expand Down
27 changes: 7 additions & 20 deletions test/everlong/EverlongTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ contract EverlongTest is VaultTest, IEverlongEvents {
strategy.setEmergencyAdmin(emergencyAdmin);
vm.stopPrank();

// Set the appropriate asset.
asset = IERC20(hyperdrive.baseToken());

// As the `management` address:
// 1. Accept the `management` role for the strategy.
// 2. Set the `profitMaxUnlockTime` to zero.
Expand All @@ -105,28 +108,12 @@ contract EverlongTest is VaultTest, IEverlongEvents {
/// @dev Deploy the Everlong Yearn v3 Vault.
function setUpEverlongVault() internal {
// As the `governance` address:
// 1. Accept the "Fee Manager" role for the Accountant.
// 2. Set the default `config.maxLoss` for the accountant to be 10%.
// This will enable losses of up to 10% across reports before
// reverting.
// 3. Deploy the Vault using the RoleManager.
// 4. Add the EverlongStrategy to the vault.
// 5. Update the max debt for the strategy to be the maximum uint256.
// 6. Configure the vault to `auto_allocate` which will automatically
// 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);
accountant.acceptFeeManager();
IAccountant.Fee memory defaultConfig = accountant.defaultConfig();
// Must increase the accountant maxLoss for reporting since `totalAssets`
// decreases whenever opening longs.
accountant.updateDefaultConfig(
0,
0,
0,
0,
defaultConfig.maxGain,
defaultConfig.maxLoss
);
vault = IVault(
roleManager.newVault(
address(asset),
Expand Down
Loading

0 comments on commit daabc00

Please sign in to comment.