Skip to content

Commit

Permalink
add support for wrapped hyperdrive tokens (#37)
Browse files Browse the repository at this point in the history
* add support for wrapped hyperdrive tokens

This enables support for hyperdrive instances with rebasing tokens since
yearn is unable to support rebasing tokens

* fixes and redeem part of test

* test fix

* fix issues with LPMath library revert on getPoolInfo

* cleanup

* responding to feedback from @jalextowle

* responding to feedback from @jalextowle

* convert "hyperdrive token" language to `execution token` language

* Update contracts/EverlongStrategy.sol

Co-authored-by: Alex Towle <[email protected]>

* correctly convert base token values from hyperdrive (#38)

Some values received from hyperdrive are denominated in base tokens.

These must be converted to vault shares token denominated values when `asBase==true || isWrapped==true` in the strategy

---------

Co-authored-by: Alex Towle <[email protected]>
  • Loading branch information
mcclurejt and jalextowle authored Dec 15, 2024
1 parent daabc00 commit 32e9c70
Show file tree
Hide file tree
Showing 11 changed files with 501 additions and 66 deletions.
288 changes: 248 additions & 40 deletions contracts/EverlongStrategy.sol

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions contracts/interfaces/IERC20Wrappable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.20;

import { IERC20 } from "openzeppelin/interfaces/IERC20.sol";

/// @author DELV
/// @title IERC20Wrappable
/// @notice Interface for an ERC20 token that can be wrapped/unwrapped.
/// @dev Since Yearn explicitly does not support rebasing tokens as
/// vault/strategy assets, wrapping is mandatory.
/// @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.
interface IERC20Wrappable is IERC20 {
/// @notice Wrap the input amount of assets.
/// @param _unwrappedAmount Amount of assets to wrap.
/// @return _wrappedAmount Amount of wrapped assets that are returned.
function wrap(
uint256 _unwrappedAmount
) external returns (uint256 _wrappedAmount);

/// @notice Unwrap the input amount of assets.
/// @param _wrappedAmount Amount of assets to unwrap.
/// @return _unwrappedAmount Amount of unwrapped assets that are returned.
function unwrap(
uint256 _wrappedAmount
) external returns (uint256 _unwrappedAmount);
}
36 changes: 36 additions & 0 deletions contracts/interfaces/IEverlongStrategy.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.20;

import { IERC20 } from "openzeppelin/interfaces/IERC20.sol";
import { IEverlongEvents } from "./IEverlongEvents.sol";
import { IPermissionedStrategy } from "./IPermissionedStrategy.sol";

Expand Down Expand Up @@ -31,6 +32,14 @@ interface IEverlongStrategy is IPermissionedStrategy, IEverlongEvents {
bytes extraData;
}

// ╭───────────────────────────────────────────────────────────────────────╮
// │ Errors │
// ╰───────────────────────────────────────────────────────────────────────╯

/// @notice Thrown when calling wrap conversion functions on a strategy with
/// a non-wrapped asset.
error AssetNotWrapped();

// ╭───────────────────────────────────────────────────────────────────────╮
// │ SETTERS │
// ╰───────────────────────────────────────────────────────────────────────╯
Expand Down Expand Up @@ -63,6 +72,30 @@ interface IEverlongStrategy is IPermissionedStrategy, IEverlongEvents {
/// @return True if a new position can be opened, false otherwise.
function canOpenPosition() external view returns (bool);

/// @notice Convert the amount of unwrapped tokens to the amount received
/// after wrapping.
/// @param _unwrappedAmount Amount of unwrapped tokens.
/// @return _wrappedAmount Amount of wrapped tokens.
function convertToWrapped(
uint256 _unwrappedAmount
) external view returns (uint256 _wrappedAmount);

/// @notice Convert the amount of wrapped tokens to the amount received
/// after unwrapping.
/// @param _wrappedAmount Amount of wrapped tokens.
/// @return _unwrappedAmount Amount of unwrapped tokens.
function convertToUnwrapped(
uint256 _wrappedAmount
) external view returns (uint256 _unwrappedAmount);

/// @notice Token used to execute trades with hyperdrive.
/// @dev Determined by `asBase`.
/// If `asBase=true`, then hyperdrive's base token is used.
/// If `asBase=false`, then hyperdrive's vault shares token is used.
/// Same as the strategy asset `asset` unless `isWrapped=true`
/// @return The token used to execute trades with hyperdrive.
function executionToken() external view returns (address);

/// @notice Reads and returns the current tend configuration from transient
/// storage.
/// @return tendEnabled Whether TendConfig has been set.
Expand All @@ -78,6 +111,9 @@ interface IEverlongStrategy is IPermissionedStrategy, IEverlongEvents {
/// @notice Gets the address of the underlying Hyperdrive Instance
function hyperdrive() external view returns (address);

/// @notice Returns whether the strategy's asset is a wrapped hyperdrive token.
function isWrapped() external view returns (bool);

/// @notice Gets the Everlong instance's kind.
/// @return The Everlong instance's kind.
function kind() external pure returns (string memory);
Expand Down
26 changes: 20 additions & 6 deletions contracts/libraries/HyperdriveExecution.sol
Original file line number Diff line number Diff line change
Expand Up @@ -492,15 +492,18 @@ library HyperdriveExecutionLibrary {
// HACK: Copied from `delvtech/hyperdrive` repo.
//
/// @dev Calculates the maximum amount of longs that can be opened.
/// @param _asBase Whether to transact using hyperdrive's base or vault
/// shares token.
/// @param _maxIterations The maximum number of iterations to use.
/// @return baseAmount The cost of buying the maximum amount of longs.
/// @return amount The cost of buying the maximum amount of longs.
function calculateMaxLong(
IHyperdrive self,
bool _asBase,
uint256 _maxIterations
) internal view returns (uint256 baseAmount) {
) internal view returns (uint256 amount) {
IHyperdrive.PoolConfig memory poolConfig = self.getPoolConfig();
IHyperdrive.PoolInfo memory poolInfo = self.getPoolInfo();
(baseAmount, ) = calculateMaxLong(
(amount, ) = calculateMaxLong(
MaxTradeParams({
shareReserves: poolInfo.shareReserves,
shareAdjustment: poolInfo.shareAdjustment,
Expand All @@ -518,17 +521,28 @@ library HyperdriveExecutionLibrary {
self.getCheckpointExposure(latestCheckpoint(self)),
_maxIterations
);
return baseAmount;

// The above `amount` is denominated in hyperdrive's base token.
// If `_asBase == false` then hyperdrive's vault shares token is being
// used and we must convert the value.
if (!_asBase) {
amount = _convertToShares(self, amount);
}

return amount;
}

// HACK: Copied from `delvtech/hyperdrive` repo.
//
/// @dev Calculates the maximum amount of longs that can be opened.
/// @param _asBase Whether to transact using hyperdrive's base or vault
/// shares token.
/// @return baseAmount The cost of buying the maximum amount of longs.
function calculateMaxLong(
IHyperdrive self
IHyperdrive self,
bool _asBase
) internal view returns (uint256 baseAmount) {
return calculateMaxLong(self, 7);
return calculateMaxLong(self, _asBase, 7);
}

// HACK: Copied from `delvtech/hyperdrive` repo.
Expand Down
26 changes: 22 additions & 4 deletions script/DeployEverlongStrategy.s.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.24;

import { IERC20 } from "openzeppelin/interfaces/IERC20.sol";
import { IHyperdrive } from "hyperdrive/contracts/src/interfaces/IHyperdrive.sol";
import { IEverlongStrategy } from "../contracts/interfaces/IEverlongStrategy.sol";
import { EVERLONG_STRATEGY_KIND, EVERLONG_STRATEGY_KEEPER_KIND } from "../contracts/libraries/Constants.sol";
Expand Down Expand Up @@ -47,6 +48,12 @@ contract DeployEverlongStrategy is BaseDeployScript {
string internal KEEPER_CONTRACT_NAME;
string internal KEEPER_CONTRACT_NAME_DEFAULT;

bool internal IS_WRAPPED;
bool internal constant IS_WRAPPED_DEFAULT = false;

address internal ASSET;
address internal ASSET_DEFAULT;

// ╭───────────────────────────────────────────────────────────────────────╮
// │ Artifact Struct. │
// ╰───────────────────────────────────────────────────────────────────────╯
Expand Down Expand Up @@ -95,6 +102,13 @@ contract DeployEverlongStrategy is BaseDeployScript {
KEEPER_CONTRACT_NAME_DEFAULT
);
output.keeperContractName = KEEPER_CONTRACT_NAME;
IS_WRAPPED = vm.envOr("IS_WRAPPED", IS_WRAPPED_DEFAULT);
ASSET_DEFAULT = address(
AS_BASE
? IHyperdrive(output.hyperdrive).baseToken()
: IHyperdrive(output.hyperdrive).vaultSharesToken()
);
ASSET = vm.envOr("ASSET", ASSET_DEFAULT);

// Validate optional arguments.
require(
Expand All @@ -106,9 +120,7 @@ contract DeployEverlongStrategy is BaseDeployScript {
).keeperContract;

// Resolve the asset address.
address asset = AS_BASE
? IHyperdrive(output.hyperdrive).baseToken()
: IHyperdrive(output.hyperdrive).vaultSharesToken();
address asset = ASSET;

// Save the strategy's kind to output.
output.kind = EVERLONG_STRATEGY_KIND;
Expand All @@ -121,7 +133,13 @@ contract DeployEverlongStrategy is BaseDeployScript {
// 5. Set the strategy's emergencyAdmin to `emergencyAdmin`.
vm.startBroadcast(DEPLOYER_PRIVATE_KEY);
output.strategy = address(
new EverlongStrategy(asset, output.name, output.hyperdrive, AS_BASE)
new EverlongStrategy(
asset,
output.name,
output.hyperdrive,
AS_BASE,
IS_WRAPPED
)
);
IEverlongStrategy(output.strategy).setPerformanceFeeRecipient(
output.governance
Expand Down
29 changes: 24 additions & 5 deletions test/everlong/EverlongTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ contract EverlongTest is VaultTest, IEverlongEvents {
/// @dev Maximum slippage for vault share price.
uint256 internal MIN_VAULT_SHARE_PRICE_SLIPPAGE = 500;

/// @dev Whether to use a wrapped asset for the strategy.
bool IS_WRAPPED;

/// @dev Asset to use for the strategy when IS_WRAPPED=true.
address WRAPPED_ASSET;

/// @dev Periphery contract to simplify maintenance operations for vaults
/// and strategies.
EverlongStrategyKeeper internal keeperContract;
Expand Down Expand Up @@ -76,12 +82,17 @@ contract EverlongTest is VaultTest, IEverlongEvents {
strategy = IPermissionedStrategy(
address(
new EverlongStrategy(
AS_BASE
? hyperdrive.baseToken()
: hyperdrive.vaultSharesToken(),
IS_WRAPPED
? WRAPPED_ASSET
: (
AS_BASE
? hyperdrive.baseToken()
: hyperdrive.vaultSharesToken()
),
EVERLONG_NAME,
address(hyperdrive),
AS_BASE
AS_BASE,
IS_WRAPPED
)
)
);
Expand All @@ -93,7 +104,15 @@ contract EverlongTest is VaultTest, IEverlongEvents {
vm.stopPrank();

// Set the appropriate asset.
asset = IERC20(hyperdrive.baseToken());
asset = (
IS_WRAPPED
? IERC20(WRAPPED_ASSET)
: IERC20(
AS_BASE
? hyperdrive.baseToken()
: hyperdrive.vaultSharesToken()
)
);

// As the `management` address:
// 1. Accept the `management` role for the strategy.
Expand Down
2 changes: 1 addition & 1 deletion test/everlong/integration/PartialClosures.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ contract TestPartialClosures is EverlongTest {
uint256 aliceDepositAmount = bound(
_deposit,
MINIMUM_TRANSACTION_AMOUNT * 100, // Increase minimum bound otherwise partial redemption won't occur
hyperdrive.calculateMaxLong()
hyperdrive.calculateMaxLong(AS_BASE)
);
uint256 aliceShares = depositStrategy(aliceDepositAmount, alice, true);
uint256 positionBondsAfterDeposit = IEverlongStrategy(address(strategy))
Expand Down
1 change: 1 addition & 0 deletions test/everlong/integration/SDAIVaultSharesToken.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ contract TestSDAIVaultSharesToken is EverlongTest {
address(asset),
"sDAI Strategy",
address(hyperdrive),
false,
false
)
)
Expand Down
16 changes: 8 additions & 8 deletions test/everlong/integration/Sandwich.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ contract TestSandwichHelper is EverlongTest {
_bystanderDeposit = bound(
_bystanderDeposit,
MINIMUM_TRANSACTION_AMOUNT * 5,
hyperdrive.calculateMaxLong() / 3
hyperdrive.calculateMaxLong(AS_BASE) / 3
);
uint256 bystanderShares = depositVault(
_bystanderDeposit,
Expand All @@ -67,7 +67,7 @@ contract TestSandwichHelper is EverlongTest {
_attackerDeposit = bound(
_attackerDeposit,
MINIMUM_TRANSACTION_AMOUNT * 5,
hyperdrive.calculateMaxLong() / 3
hyperdrive.calculateMaxLong(AS_BASE) / 3
);
uint256 attackerShares = depositVault(
_attackerDeposit,
Expand Down Expand Up @@ -125,7 +125,7 @@ contract TestSandwichHelper is EverlongTest {
_bystanderDeposit = bound(
_bystanderDeposit,
MINIMUM_TRANSACTION_AMOUNT * 5,
hyperdrive.calculateMaxLong() / 3
hyperdrive.calculateMaxLong(AS_BASE) / 3
);
uint256 bystanderShares = depositVault(
_bystanderDeposit,
Expand All @@ -139,7 +139,7 @@ contract TestSandwichHelper is EverlongTest {
_attackerDeposit = bound(
_attackerDeposit,
MINIMUM_TRANSACTION_AMOUNT * 5,
hyperdrive.calculateMaxLong() / 3
hyperdrive.calculateMaxLong(AS_BASE) / 3
);
uint256 attackerShares = depositVault(
_attackerDeposit,
Expand Down Expand Up @@ -212,7 +212,7 @@ contract TestSandwichHelper is EverlongTest {
_bystanderDeposit = bound(
_bystanderDeposit,
MINIMUM_TRANSACTION_AMOUNT * 5,
hyperdrive.calculateMaxLong() / 3
hyperdrive.calculateMaxLong(AS_BASE) / 3
);
uint256 bystanderEverlongShares = depositVault(
_bystanderDeposit,
Expand All @@ -226,7 +226,7 @@ contract TestSandwichHelper is EverlongTest {
_attackerDeposit = bound(
_attackerDeposit,
MINIMUM_TRANSACTION_AMOUNT * 5,
hyperdrive.calculateMaxLong() / 3
hyperdrive.calculateMaxLong(AS_BASE) / 3
);
uint256 attackerEverlongShares = depositVault(
_attackerDeposit,
Expand Down Expand Up @@ -279,7 +279,7 @@ contract TestSandwichHelper is EverlongTest {
_bystanderDeposit = bound(
_bystanderDeposit,
MINIMUM_TRANSACTION_AMOUNT * 5,
hyperdrive.calculateMaxLong() / 3
hyperdrive.calculateMaxLong(AS_BASE) / 3
);
uint256 bystanderShares = depositVault(
_bystanderDeposit,
Expand All @@ -293,7 +293,7 @@ contract TestSandwichHelper is EverlongTest {
_attackerDeposit = bound(
_attackerDeposit,
MINIMUM_TRANSACTION_AMOUNT * 5,
hyperdrive.calculateMaxLong() / 3
hyperdrive.calculateMaxLong(AS_BASE) / 3
);
uint256 attackerShares = depositVault(
_attackerDeposit,
Expand Down
4 changes: 2 additions & 2 deletions test/everlong/units/HyperdriveExecution.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ contract TestHyperdriveExecution is EverlongTest {
// decrease the value of the share adjustment to a non-trivial value.
matureLongAmount = matureLongAmount.normalizeToRange(
MINIMUM_TRANSACTION_AMOUNT + 1,
hyperdrive.calculateMaxLong() / 2
hyperdrive.calculateMaxLong(AS_BASE) / 2
);
openLong(alice, matureLongAmount);
advanceTime(hyperdrive.getPoolConfig().positionDuration, 0);
Expand Down Expand Up @@ -343,7 +343,7 @@ contract TestHyperdriveExecution is EverlongTest {
// value which stress tests the max long function.
initialLongAmount = initialLongAmount.normalizeToRange(
MINIMUM_TRANSACTION_AMOUNT + 1,
hyperdrive.calculateMaxLong() / 2
hyperdrive.calculateMaxLong(AS_BASE) / 2
);
openLong(bob, initialLongAmount);
initialShortAmount = initialShortAmount.normalizeToRange(
Expand Down
Loading

0 comments on commit 32e9c70

Please sign in to comment.