From cc5c2027a2e54e97aa5188d833f7a46cb294c6db Mon Sep 17 00:00:00 2001 From: johnnyonline Date: Mon, 23 Sep 2024 23:28:56 +0900 Subject: [PATCH] feat: refactor token wrapping --- src/interfaces/IWrapper.sol | 19 +++++ src/ynEIGEN/EigenStrategyManager.sol | 32 +++----- src/ynEIGEN/LSDWrapper.sol | 78 +++++++++++++++++++ src/ynEIGEN/TokenStakingNode.sol | 32 ++------ src/ynEIGEN/ynEigenDepositAdapter.sol | 15 +++- .../ynEIGEN/ynLSDeDepositAdapter.t.sol | 55 +++++++++++++ .../scenarios/ynEIGEN/ynLSDeWithdrawals.t.sol | 43 +++++++--- 7 files changed, 209 insertions(+), 65 deletions(-) create mode 100644 src/interfaces/IWrapper.sol create mode 100644 src/ynEIGEN/LSDWrapper.sol create mode 100644 test/scenarios/ynEIGEN/ynLSDeDepositAdapter.t.sol diff --git a/src/interfaces/IWrapper.sol b/src/interfaces/IWrapper.sol new file mode 100644 index 000000000..232ccb551 --- /dev/null +++ b/src/interfaces/IWrapper.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; + +interface IWrapper { + + /// @notice Wraps the given amount of the given token. + /// @param _amount The amount to wrap. + /// @param _token The token to wrap. + /// @return The amount of wrapped tokens and the wrapped token. + function wrap(uint256 _amount, IERC20 _token) external returns (uint256, IERC20); + + /// @notice Unwraps the given amount of the given token. + /// @param _amount The amount to unwrap. + /// @param _token The token to unwrap. + /// @return The amount of unwrapped tokens and the unwrapped token. + function unwrap(uint256 _amount, IERC20 _token) external returns (uint256, IERC20); +} \ No newline at end of file diff --git a/src/ynEIGEN/EigenStrategyManager.sol b/src/ynEIGEN/EigenStrategyManager.sol index d1e1f2c0b..219d0188b 100644 --- a/src/ynEIGEN/EigenStrategyManager.sol +++ b/src/ynEIGEN/EigenStrategyManager.sol @@ -15,6 +15,7 @@ import {IynEigen} from "src/interfaces/IynEigen.sol"; import {IwstETH} from "src/external/lido/IwstETH.sol"; import {IERC4626} from "lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IWrapper} from "src/interfaces/IWrapper.sol"; interface IEigenStrategyManagerEvents { event StrategyAdded(address indexed asset, address indexed strategy); @@ -107,6 +108,7 @@ contract EigenStrategyManager is IERC20 public stETH; IRedemptionAssetsVaultExt public redemptionAssetsVault; + IWrapper public wrapper; //-------------------------------------------------------------------------------------- //---------------------------------- INITIALIZATION ---------------------------------- @@ -166,11 +168,17 @@ contract EigenStrategyManager is function initializeV2( address _redemptionAssetsVault, + address _wrapper, address _withdrawer - ) external reinitializer(2) notZeroAddress(_redemptionAssetsVault) { + ) external reinitializer(2) notZeroAddress(_redemptionAssetsVault) notZeroAddress(_wrapper) { redemptionAssetsVault = IRedemptionAssetsVaultExt(_redemptionAssetsVault); + wrapper = IWrapper(_wrapper); + _grantRole(STAKING_NODES_WITHDRAWER_ROLE, _withdrawer); _grantRole(WITHDRAWAL_MANAGER_ROLE, _withdrawer); + + IERC20(address(wstETH)).forceApprove(address(_wrapper), type(uint256).max); + IERC20(address(woETH)).forceApprove(address(_wrapper), type(uint256).max); } //-------------------------------------------------------------------------------------- @@ -239,7 +247,7 @@ contract EigenStrategyManager is uint256[] memory depositAmounts = new uint256[](amountsLength); for (uint256 i = 0; i < assetsLength; i++) { - (IERC20 depositAsset, uint256 depositAmount) = toEigenLayerDeposit(assets[i], amounts[i]); + (uint256 depositAmount, IERC20 depositAsset) = wrapper.unwrap(amounts[i], assets[i]); depositAssets[i] = depositAsset; depositAmounts[i] = depositAmount; @@ -254,26 +262,6 @@ contract EigenStrategyManager is emit DepositedToEigenlayer(depositAssets, depositAmounts, strategiesForNode); } - function toEigenLayerDeposit( - IERC20 asset, - uint256 amount - ) internal returns (IERC20 depositAsset, uint256 depositAmount) { - if (address(asset) == address(wstETH)) { - // Adjust for wstETH - depositAsset = stETH; - depositAmount = wstETH.unwrap(amount); - } else if (address(asset) == address(woETH)) { - // Adjust for woeth - depositAsset = oETH; - // calling redeem with receiver and owner as address(this) - depositAmount = woETH.redeem(amount, address(this), address(this)); - } else { - // No adjustment needed - depositAsset = asset; - depositAmount = amount; - } - } - //-------------------------------------------------------------------------------------- //---------------------------------- WITHDRAWALS ------------------------------------- //-------------------------------------------------------------------------------------- diff --git a/src/ynEIGEN/LSDWrapper.sol b/src/ynEIGEN/LSDWrapper.sol new file mode 100644 index 000000000..6a08f50eb --- /dev/null +++ b/src/ynEIGEN/LSDWrapper.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {IERC4626} from "lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; +import {IERC20, SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IwstETH} from "src/external/lido/IwstETH.sol"; +import {IWrapper} from "src/interfaces/IWrapper.sol"; + +contract LSDWrapper is IWrapper, Initializable { + + using SafeERC20 for IERC20; + + IERC20 public immutable wstETH; + IERC20 public immutable woETH; + IERC20 public immutable oETH; + IERC20 public immutable stETH; + + // ============================================================================================ + // Constructor + // ============================================================================================ + + constructor(address _wstETH, address _woETH, address _oETH, address _stETH) { + if (_wstETH == address(0) || _woETH == address(0) || _oETH == address(0) || _stETH == address(0)) { + revert ZeroAddress(); + } + + wstETH = IERC20(_wstETH); + woETH = IERC20(_woETH); + oETH = IERC20(_oETH); + stETH = IERC20(_stETH); + } + + function initialize() external initializer { + stETH.forceApprove(address(wstETH), type(uint256).max); + oETH.forceApprove(address(woETH), type(uint256).max); + } + + // ============================================================================================ + // External functions + // ============================================================================================ + + /// @inheritdoc IWrapper + function wrap(uint256 _amount, IERC20 _token) external returns (uint256, IERC20) { + if (_token == stETH) { + stETH.safeTransferFrom(msg.sender, address(this), _amount); + _amount = IwstETH(address(wstETH)).wrap(_amount); + wstETH.safeTransfer(msg.sender, _amount); + return (_amount, wstETH); + } else if (_token == oETH) { + oETH.safeTransferFrom(msg.sender, address(this), _amount); + return (IERC4626(address(woETH)).deposit(_amount, msg.sender), woETH); + } else { + return (_amount, _token); + } + } + + /// @inheritdoc IWrapper + function unwrap(uint256 _amount, IERC20 _token) external returns (uint256, IERC20) { + if (_token == wstETH) { + wstETH.safeTransferFrom(msg.sender, address(this), _amount); + _amount = IwstETH(address(wstETH)).unwrap(_amount); + stETH.safeTransfer(msg.sender, _amount); + return (_amount, stETH); + } else if (_token == woETH) { + return (IERC4626(address(woETH)).redeem(_amount, msg.sender, msg.sender), oETH); + } else { + return (_amount, _token); + } + } + + // ============================================================================================ + // Errors + // ============================================================================================ + + error ZeroAddress(); +} \ No newline at end of file diff --git a/src/ynEIGEN/TokenStakingNode.sol b/src/ynEIGEN/TokenStakingNode.sol index 41f925387..c15a5e979 100644 --- a/src/ynEIGEN/TokenStakingNode.sol +++ b/src/ynEIGEN/TokenStakingNode.sol @@ -12,8 +12,7 @@ import {IDelegationManager} from "lib/eigenlayer-contracts/src/contracts/interfa import {IStrategy} from "lib/eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; import {ITokenStakingNode} from "src/interfaces/ITokenStakingNode.sol"; import {ITokenStakingNodesManager} from "src/interfaces/ITokenStakingNodesManager.sol"; -import {IwstETH} from "src/external/lido/IwstETH.sol"; -import {IERC4626} from "lib/openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; +import {IWrapper} from "src/interfaces/IWrapper.sol"; interface ITokenStakingNodeEvents { event DepositToEigenlayer( @@ -30,10 +29,7 @@ interface ITokenStakingNodeEvents { } interface IYieldNestStrategyManager { - function wstETH() external view returns (IwstETH); - function stETH() external view returns (IERC20); - function woETH() external view returns (IERC4626); - function oETH() external view returns (IERC20); + function wrapper() external view returns (IWrapper); function isStakingNodesWithdrawer(address _address) external view returns (bool); } @@ -206,7 +202,9 @@ contract TokenStakingNode is _expectedAmountOut - _actualAmountOut; if (_delta > 2) revert WithdrawalAmountMismatch(); // @todo - might be a footgun - (_actualAmountOut, _token) = _wrapIfNeeded(_actualAmountOut, _token); + IWrapper _wrapper = IYieldNestStrategyManager(tokenStakingNodesManager.yieldNestStrategyManager()).wrapper(); + IERC20(_token).forceApprove(address(_wrapper), _actualAmountOut); // NOTE: approving also token that will not be transferred + (_actualAmountOut, _token) = _wrapper.wrap(_actualAmountOut, _token); queuedShares[_strategy] -= _shares; withdrawn[_token] += _actualAmountOut; @@ -222,26 +220,6 @@ contract TokenStakingNode is emit DeallocatedTokens(_amount, _token); } - function _wrapIfNeeded(uint256 _amount, IERC20 _token) internal returns (uint256, IERC20) { - IYieldNestStrategyManager _strategyManager = - IYieldNestStrategyManager(ITokenStakingNodesManager(address(tokenStakingNodesManager)).yieldNestStrategyManager()); - IERC20 _stETH = _strategyManager.stETH(); - IERC20 _oETH = _strategyManager.oETH(); - if (_token == _stETH) { - IwstETH _wstETH = _strategyManager.wstETH(); - _stETH.forceApprove(address(_wstETH), _amount); - uint256 _wstETHAmount = _wstETH.wrap(_amount); - return (_wstETHAmount, IERC20(_wstETH)); - } else if (_token == _oETH) { - IERC4626 _woETH = _strategyManager.woETH(); - _oETH.forceApprove(address(_woETH), _amount); - uint256 _woETHShares = _woETH.deposit(_amount, address(this)); - return (_woETHShares, IERC20(_woETH)); - } else { - return (_amount, _token); - } - } - //-------------------------------------------------------------------------------------- //---------------------------------- DELEGATION -------------------------------------- //-------------------------------------------------------------------------------------- diff --git a/src/ynEIGEN/ynEigenDepositAdapter.sol b/src/ynEIGEN/ynEigenDepositAdapter.sol index 2e5a709f9..21d574336 100644 --- a/src/ynEIGEN/ynEigenDepositAdapter.sol +++ b/src/ynEIGEN/ynEigenDepositAdapter.sol @@ -10,6 +10,8 @@ import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/ import {AccessControlUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol"; import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {IWrapper} from "src/interfaces/IWrapper.sol"; + interface IynEigenDepositAdapterEvents { event ReferralDepositProcessed( address sender, @@ -54,6 +56,7 @@ contract ynEigenDepositAdapter is IynEigenDepositAdapterEvents, Initializable, A IERC4626 public woETH; IERC20 public stETH; IERC20 public oETH; + IWrapper public wrapper; //-------------------------------------------------------------------------------------- //---------------------------------- INITIALIZATION ---------------------------------- @@ -84,6 +87,10 @@ contract ynEigenDepositAdapter is IynEigenDepositAdapterEvents, Initializable, A oETH = IERC20(woETH.asset()); } + function initializeV2(address _wrapper) external reinitializer(2) notZeroAddress(_wrapper) { + wrapper = IWrapper(_wrapper); + } + /** * @notice Handles the deposit of assets into the ynEigen system. It supports all assets supported by ynEigen @@ -159,8 +166,8 @@ contract ynEigenDepositAdapter is IynEigenDepositAdapterEvents, Initializable, A function depositStETH(uint256 amount, address receiver) internal returns (uint256 shares) { stETH.safeTransferFrom(msg.sender, address(this), amount); - stETH.forceApprove(address(wstETH), amount); - uint256 wstETHAmount = wstETH.wrap(amount); + stETH.forceApprove(address(wrapper), amount); + (uint256 wstETHAmount,) = wrapper.wrap(amount, stETH); wstETH.forceApprove(address(ynEigen), wstETHAmount); shares = ynEigen.deposit(IERC20(address(wstETH)), wstETHAmount, receiver); @@ -170,8 +177,8 @@ contract ynEigenDepositAdapter is IynEigenDepositAdapterEvents, Initializable, A function depositOETH(uint256 amount, address receiver) internal returns (uint256 shares) { oETH.safeTransferFrom(msg.sender, address(this), amount); - oETH.forceApprove(address(woETH), amount); - uint256 woETHShares = woETH.deposit(amount, address(this)); + oETH.forceApprove(address(wrapper), amount); + (uint256 woETHShares,) = wrapper.wrap(amount, oETH); woETH.forceApprove(address(ynEigen), woETHShares); shares = ynEigen.deposit(IERC20(address(woETH)), woETHShares, receiver); diff --git a/test/scenarios/ynEIGEN/ynLSDeDepositAdapter.t.sol b/test/scenarios/ynEIGEN/ynLSDeDepositAdapter.t.sol new file mode 100644 index 000000000..688955220 --- /dev/null +++ b/test/scenarios/ynEIGEN/ynLSDeDepositAdapter.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {TestAssetUtils} from "test/utils/TestAssetUtils.sol"; + +import "./ynLSDeWithdrawals.t.sol"; + +contract ynLSDeDepositAdapterTest is ynLSDeWithdrawalsTest { + + TestAssetUtils public testAssetUtils; + + function setUp() public override { + super.setUp(); + + // upgrade deposit adapter + { + _upgradeContract( + address(ynEigenDepositAdapter_), + address(new ynEigenDepositAdapter()), + abi.encodeWithSignature("initializeV2(address)", address(wrapper)) + ); + } + + // deploy testAssetUtils + { + testAssetUtils = new TestAssetUtils(); + } + } + + // function testDepositSTETH(uint256 _amount) public { + // vm.assume(_amount > 0 && _amount <= 10 ether); + + // testAssetUtils.get_stETH(user, _amount); + + // vm.startPrank(user); + // IERC20(chainAddresses.lsd.STETH_ADDRESS).approve(address(ynEigenDepositAdapter_), _amount); + // uint256 _ynOut = ynEigenDepositAdapter_.deposit(IERC20(chainAddresses.lsd.STETH_ADDRESS), _amount, user); + // vm.stopPrank(); + + // assertEq(IERC20(yneigen).balanceOf(user), _ynOut, "testDepositSTETH"); + // } + + function testDepositOETH(uint256 _amount) public { + vm.assume(_amount > 0 && _amount <= 10 ether); + + testAssetUtils.get_OETH(user, _amount); + + vm.startPrank(user); + IERC20(chainAddresses.lsd.OETH_ADDRESS).approve(address(ynEigenDepositAdapter_), _amount); + uint256 _ynOut = ynEigenDepositAdapter_.deposit(IERC20(chainAddresses.lsd.OETH_ADDRESS), _amount, user); + vm.stopPrank(); + + assertEq(IERC20(yneigen).balanceOf(user), _ynOut, "testDepositOETH"); + } +} \ No newline at end of file diff --git a/test/scenarios/ynEIGEN/ynLSDeWithdrawals.t.sol b/test/scenarios/ynEIGEN/ynLSDeWithdrawals.t.sol index aba2269ca..7933025e1 100644 --- a/test/scenarios/ynEIGEN/ynLSDeWithdrawals.t.sol +++ b/test/scenarios/ynEIGEN/ynLSDeWithdrawals.t.sol @@ -11,6 +11,7 @@ import {ITokenStakingNode} from "src/interfaces/ITokenStakingNode.sol"; import {IRedeemableAsset} from "src/interfaces/IRedeemableAsset.sol"; import {IYieldNestStrategyManager} from "src/interfaces/IYieldNestStrategyManager.sol"; +import {LSDWrapper} from "src/ynEIGEN/LSDWrapper.sol"; import {RedemptionAssetsVault} from "src/ynEIGEN/RedemptionAssetsVault.sol"; import {WithdrawalQueueManager} from "src/WithdrawalQueueManager.sol"; @@ -25,15 +26,16 @@ contract ynLSDeWithdrawalsTest is ynLSDeScenarioBaseTest { ITokenStakingNode public tokenStakingNode; RedemptionAssetsVault public redemptionAssetsVault; WithdrawalQueueManager public withdrawalQueueManager; + LSDWrapper public wrapper; uint256 public constant AMOUNT = 1 ether; - function setUp() public override { + function setUp() public virtual override { - vm.createSelectFork( - "https://eth-mainnet.g.alchemy.com/v2/GWBlcyYZH65PHOKw_l-9pvqYdwJFPo4-", // rpc url - 20782621 // fork block number - ); + // vm.createSelectFork( + // "", // rpc url + // 20782621 // fork block number + // ); super.setUp(); uint256 _totalAssetsBefore = yneigen.totalAssets(); @@ -45,22 +47,22 @@ contract ynLSDeWithdrawalsTest is ynLSDeScenarioBaseTest { // upgrade ynLSDe { - _upgradeContract(address(yneigen), address(new ynEigen())); + _upgradeContract(address(yneigen), address(new ynEigen()), ""); } // upgrade EigenStrategyManager { - _upgradeContract(address(eigenStrategyManager), address(new EigenStrategyManager())); + _upgradeContract(address(eigenStrategyManager), address(new EigenStrategyManager()), ""); } // upgrade AssetRegistry { - _upgradeContract(address(assetRegistry), address(new AssetRegistry())); + _upgradeContract(address(assetRegistry), address(new AssetRegistry()), ""); } // upgrade TokenStakingNodesManager { - _upgradeContract(address(tokenStakingNodesManager), address(new TokenStakingNodesManager())); + _upgradeContract(address(tokenStakingNodesManager), address(new TokenStakingNodesManager()), ""); } // deal assets to user @@ -90,9 +92,26 @@ contract ynLSDeWithdrawalsTest is ynLSDeScenarioBaseTest { withdrawalQueueManager = WithdrawalQueueManager(address(_proxy)); } + // deploy wrapper + { + // call `initialize` on LSDWrapper + TransparentUpgradeableProxy _proxy = new TransparentUpgradeableProxy( + address( + new LSDWrapper( + chainAddresses.lsd.WSTETH_ADDRESS, + chainAddresses.lsd.WOETH_ADDRESS, + chainAddresses.lsd.OETH_ADDRESS, + chainAddresses.lsd.STETH_ADDRESS) + ), + actors.admin.PROXY_ADMIN_OWNER, + abi.encodeWithSignature("initialize()") + ); + wrapper = LSDWrapper(address(_proxy)); + } + // initialize eigenStrategyManager { - eigenStrategyManager.initializeV2(address(redemptionAssetsVault), actors.ops.WITHDRAWAL_MANAGER); + eigenStrategyManager.initializeV2(address(redemptionAssetsVault), address(wrapper), actors.ops.WITHDRAWAL_MANAGER); } // initialize RedemptionAssetsVault @@ -550,12 +569,12 @@ contract ynLSDeWithdrawalsTest is ynLSDeScenarioBaseTest { assertEq(yneigen.totalSupply(), previousTotalSupply, "Total supply mismatch after upgrade"); } - function _upgradeContract(address _proxyAddress, address _newImplementation) private { + function _upgradeContract(address _proxyAddress, address _newImplementation, bytes memory _data) internal { bytes memory _data = abi.encodeWithSignature( "upgradeAndCall(address,address,bytes)", _proxyAddress, // proxy _newImplementation, // implementation - "" // no data + _data ); vm.startPrank(actors.wallets.YNSecurityCouncil); timelockController.schedule(