Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for wrapped hyperdrive tokens #37

Merged
merged 10 commits into from
Dec 15, 2024
313 changes: 268 additions & 45 deletions contracts/EverlongStrategy.sol

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions contracts/EverlongStrategyKeeper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,12 @@ contract EverlongStrategyKeeper is Ownable {
return 0;
}

// If the strategy is a wrapped token, convert the spend amount to
// be denominated in hyperdrive's token.
mcclurejt marked this conversation as resolved.
Show resolved Hide resolved
if (IEverlongStrategy(_strategy).isWrapped()) {
canSpend = IEverlongStrategy(_strategy).convertToWrapped(canSpend);
}

// Calculate the amount of bonds that would be received if a long was
// opened with `canSpend` assets.
uint256 expectedOutput = hyperdrive.previewOpenLong(
Expand Down
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);
}
28 changes: 28 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,22 @@ 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);

jalextowle marked this conversation as resolved.
Show resolved Hide resolved
/// @notice Reads and returns the current tend configuration from transient
/// storage.
/// @return tendEnabled Whether TendConfig has been set.
Expand All @@ -78,6 +103,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: 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
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
111 changes: 111 additions & 0 deletions test/everlong/units/Wrapping.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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 { IERC20Wrappable } from "../../../contracts/interfaces/IERC20Wrappable.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 Tests wrapping functionality for rebasing tokens.
contract TestWrapping is EverlongTest {
using FixedPointMath for *;

/// @dev The StETH token address.
address internal constant STETH =
0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;

/// @dev The WStETH address.
address constant WSTETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;

// @dev StETH hyperdrive address.
address constant STETH_HYPERDRIVE =
0xd7e470043241C10970953Bd8374ee6238e77D735;

/// @dev The StETH whale address.
address internal constant STETH_WHALE =
0x1982b2F5814301d4e9a8b0201555376e62F82428;

/// @dev Using the standard set up process with the mainnet
/// `StETHHyperdrive` instance leads to issues with the `LPMath`
/// library. To avoid this, we have to use a custom `setUp` that
/// does not attempt to deploy a test instance of hyperdrive.
function setUp() public virtual override {
vm.createSelectFork(vm.rpcUrl("mainnet"), FORK_BLOCK_NUMBER);
(alice, ) = createUser("alice");
(bob, ) = createUser("bob");
(governance, ) = createUser("governance");
(management, ) = createUser("management");
(deployer, ) = createUser("deployer");

AS_BASE = false;
IS_WRAPPED = true;
WRAPPED_ASSET = WSTETH;
hyperdrive = IHyperdrive(STETH_HYPERDRIVE);

setUpRoleManager();
setUpEverlongStrategy();
setUpEverlongVault();
}

/// @dev Mint some WStETH to the specified account.
function mint(address _to, uint256 _amount) internal {
vm.startPrank(STETH_WHALE);
IERC20(STETH).approve(WSTETH, type(uint256).max);
IERC20Wrappable(WSTETH).wrap(
IEverlongStrategy(address(strategy))
.convertToUnwrapped(_amount)
.mulUp(1.001e18)
);
IERC20Wrappable(WSTETH).transfer(_to, _amount);
vm.stopPrank();
}

/// @dev Deposit the specified amount of wrapped assets into the vault.
function depositWrapped(
address _from,
uint256 _amount
) internal returns (uint256 shares) {
mint(_from, _amount);
vm.startPrank(_from);
IERC20Wrappable(WSTETH).approve(address(vault), _amount + 1);
shares = vault.deposit(_amount, _from);
vm.stopPrank();
}

/// @dev Redeem the specified amount of shares from the vault.
function redeemWrapped(
address _from,
uint256 _shares
) internal returns (uint256 assets) {
vm.startPrank(_from);

assets = vault.redeem(_shares, _from, _from);
vm.stopPrank();
}

/// @dev Ensure the deposit and redeem functions work as expected.
function test_deposit_redeem() external {
// Alice deposits into the vault.
uint256 depositAmount = 100e18;
uint256 aliceShares = depositWrapped(alice, depositAmount);
uint256 bobShares = depositWrapped(bob, depositAmount);

// Alice and Bob should have non-zero share amounts.
assertGt(aliceShares, 0);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you're starting with a specific fork block number in VaultTest, you should be able to assert on an exact number consistently here.

assertGt(bobShares, 0);

// Call update_debt and tend.
rebalance();

// Alice and Bob redeem from the vault.
uint256 aliceProceeds = redeemWrapped(alice, aliceShares);
uint256 bobProceeds = redeemWrapped(bob, bobShares);

// Alice and Bob should have within 1% of their starting balance.
assertApproxEqRel(depositAmount, aliceProceeds, 0.01e18);
assertApproxEqRel(depositAmount, bobProceeds, 0.01e18);
}
}
Loading