From 620352ec3f7c790e992f7e2ea8a53222de46c17d Mon Sep 17 00:00:00 2001 From: Albert Su Date: Wed, 27 Mar 2024 21:12:19 -0700 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9C=85Create=20renzo=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interfaces/IRenzo.sol | 3 + src/strategies/RenzoStrategy.sol | 101 ++++++++++++++++++++++++++++ test/Strategies/RenzoStrategy.t.sol | 88 ++++++++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 src/interfaces/IRenzo.sol create mode 100644 src/strategies/RenzoStrategy.sol create mode 100644 test/Strategies/RenzoStrategy.t.sol diff --git a/src/interfaces/IRenzo.sol b/src/interfaces/IRenzo.sol new file mode 100644 index 0000000..b781e10 --- /dev/null +++ b/src/interfaces/IRenzo.sol @@ -0,0 +1,3 @@ +interface IRenzo { + function depositETH() external payable; +} diff --git a/src/strategies/RenzoStrategy.sol b/src/strategies/RenzoStrategy.sol new file mode 100644 index 0000000..53356fd --- /dev/null +++ b/src/strategies/RenzoStrategy.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23; + +import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { IStakingStrategy } from "../interfaces/IStakingStrategy.sol"; +import { IRenzo } from "../interfaces/IRenzo.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgradeable { + address public stakingManager; + bool public autoStake; + uint256 public ethUnderWithdrawal; + IRenzo public renzo; + IERC20 public ezETH; + uint256[50] private __gap; + + event EthStaked(uint256 amount); + event EthWithdrawn(uint256 amount); + event SetStakingManager(address stakingManager); + event SetAutoStake(bool autoStake); + + error InsufficientFunds(); + error TransferFailed(bytes data); + error OnlyStakingManager(address sender); + error RenzoWithdrawalsNotLive(); + + modifier onlyStakingManager() { + if (msg.sender != stakingManager) revert OnlyStakingManager(msg.sender); + _; + } + + constructor() { + _disableInitializers(); + } + + function initialize(address _owner, address _stakingManager) external initializer { + stakingManager = _stakingManager; + autoStake = true; + __Ownable2Step_init(); + __UUPSUpgradeable_init(); + _transferOwnership(_owner); + renzo = IRenzo(0x74a09653A083691711cF8215a6ab074BB4e99ef5); + ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); + } + + /// -------------------------------- 📝 External Functions 📝 -------------------------------- + function deposit(uint256 amount) external payable override onlyStakingManager { + if (!autoStake) return; + _deposit(amount); + } + + function withdraw(uint256 amount) external override onlyStakingManager returns (uint256 withdrawnAmount) { + uint256 balance = address(this).balance; + if (amount > balance) { + withdrawnAmount = balance; + } else { + withdrawnAmount = amount; + } + return _withdraw(withdrawnAmount); + } + + /// --------------------------------- 🛠️ Internal Functions 🛠️ --------------------------------- + function _deposit(uint256 amount) internal { + if (amount > address(this).balance) revert InsufficientFunds(); + renzo.depositETH{ value: amount }(); + emit EthStaked(amount); + } + + function _withdraw(uint256 withdrawnAmount) internal returns (uint256) { + revert RenzoWithdrawalsNotLive(); + } + + /// ---------------------------------- 🔓 Admin Functions 🔓 ---------------------------------- + function ownerDeposit(uint256 amount) external payable override onlyOwner { + _deposit(amount); + } + + function ownerWithdraw(uint256 amount) external override onlyOwner returns (uint256 withdrawnAmount) { + return _withdraw(amount); + } + + function setStakingManager(address _stakingManager) external override onlyOwner { + stakingManager = _stakingManager; + emit SetStakingManager(_stakingManager); + } + + function setAutoStake(bool _autoStake) external override onlyOwner { + autoStake = _autoStake; + emit SetAutoStake(_autoStake); + } + + /// --------------------------------- 🔎 View Functions 🔍 --------------------------------- + function underlyingAssetAmount() external view override returns (uint256) { + return address(this).balance + ezETH.balanceOf(address(this)); + } + + receive() external payable { } + + function _authorizeUpgrade(address) internal override onlyOwner { } +} diff --git a/test/Strategies/RenzoStrategy.t.sol b/test/Strategies/RenzoStrategy.t.sol new file mode 100644 index 0000000..adce973 --- /dev/null +++ b/test/Strategies/RenzoStrategy.t.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23 <0.9.0; + +import "forge-std/src/Vm.sol"; +import { PRBTest } from "@prb/test/src/PRBTest.sol"; +import { console2 } from "forge-std/src/console2.sol"; +import { StdCheats } from "forge-std/src/StdCheats.sol"; +import { StdUtils } from "forge-std/src/StdUtils.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import { EdgelessDeposit } from "../../src/EdgelessDeposit.sol"; +import { StakingManager } from "../../src/StakingManager.sol"; +import { WrappedToken } from "../../src/WrappedToken.sol"; +import { RenzoStrategy } from "../../src/strategies/RenzoStrategy.sol"; + +import { IWithdrawalQueueERC721 } from "../../src/interfaces/IWithdrawalQueueERC721.sol"; +import { IStakingStrategy } from "../../src/interfaces/IStakingStrategy.sol"; + +import { Permit, SigUtils } from "../Utils/SigUtils.sol"; +import { DeploymentUtils } from "../Utils/DeploymentUtils.sol"; + +/// @dev If this is your first time with Forge, read this tutorial in the Foundry Book: +/// https://book.getfoundry.sh/forge/writing-tests +contract RenzoStrategyTest is PRBTest, StdCheats, StdUtils, DeploymentUtils { + using SigUtils for Permit; + + EdgelessDeposit internal edgelessDeposit; + WrappedToken internal wrappedEth; + StakingManager internal stakingManager; + IStakingStrategy internal renzoStrategy; + + uint32 public constant FORK_BLOCK_NUMBER = 18_950_000; + + address public owner = makeAddr("Edgeless owner"); + address public depositor = makeAddr("Depositor"); + IERC20 ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); + + /// @dev A function invoked before each test case is run. + function setUp() public virtual { + string memory alchemyApiKey = vm.envOr("API_KEY_ALCHEMY", string("")); + vm.createSelectFork({ + urlOrAlias: string(abi.encodePacked("https://Eth-mainnet.g.alchemy.com/v2/", alchemyApiKey)), + blockNumber: FORK_BLOCK_NUMBER + }); + + (stakingManager, edgelessDeposit, wrappedEth,) = deployContracts(owner); + vm.startPrank(owner); + address RenzoStrategyImpl = address(new RenzoStrategy()); + bytes memory RenzoStrategyData = abi.encodeCall(RenzoStrategy.initialize, (owner, address(stakingManager))); + renzoStrategy = IStakingStrategy(payable(address(new ERC1967Proxy(RenzoStrategyImpl, RenzoStrategyData)))); + stakingManager.addStrategy(stakingManager.ETH_ADDRESS(), renzoStrategy); + stakingManager.setActiveStrategy(stakingManager.ETH_ADDRESS(), 1); + vm.stopPrank(); + } + + function test_DepositToRenzo(uint256 amount) external { + amount = bound(amount, 1 ether, 100 ether); + vm.prank(owner); + renzoStrategy.setAutoStake(true); + vm.startPrank(depositor); + vm.deal(depositor, amount); + + // Deposit Eth + edgelessDeposit.depositEth{ value: amount }(depositor); + assertEq( + address(depositor).balance, + 0, + "Deposit should have 0 Eth since all Eth was sent to the edgeless edgelessDeposit contract" + ); + assertEq(wrappedEth.balanceOf(depositor), amount, "Depositor should have `amount` of wrapped Eth"); + assertEq(address(renzoStrategy).balance, 0, "EthStrategy should have 0 Eth"); + assertTrue( + isWithinPercentage(ezETH.balanceOf(address(renzoStrategy)), amount, 5), "EthStrategy should have 0 Eth" + ); + vm.stopPrank(); + } + + function isWithinPercentage(uint256 value1, uint256 value2, uint8 percentage) internal pure returns (bool) { + require(percentage > 0 && percentage <= 100, "Percentage must be between 1 and 100"); + + // Calculate the margin of error + uint256 margin = (value1 * percentage) / 100; + + // Check if value2 is within the acceptable range + return value2 >= value1 - margin && value2 <= value1 + margin; + } +} From 2de8b718ca8efc244b870bbe21153bce8cae6dbc Mon Sep 17 00:00:00 2001 From: Albert Su Date: Wed, 27 Mar 2024 21:21:33 -0700 Subject: [PATCH 02/10] =?UTF-8?q?=E2=9C=85Add=20deploy=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/ethereum/002_deployRenzoIntegration.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 deploy/ethereum/002_deployRenzoIntegration.ts diff --git a/deploy/ethereum/002_deployRenzoIntegration.ts b/deploy/ethereum/002_deployRenzoIntegration.ts new file mode 100644 index 0000000..e50d699 --- /dev/null +++ b/deploy/ethereum/002_deployRenzoIntegration.ts @@ -0,0 +1,58 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; +import * as WrappedTokenArtifact from "../../artifacts/src/WrappedToken.sol/WrappedToken.json"; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre; + const { deploy, execute, get, getOrNull, log, read, save } = deployments; + const { deployer } = await getNamedAccounts(); + + const EdgelessDeposit = await getOrNull("EdgelessDeposit"); + if (!EdgelessDeposit) { + await deploy("RenzoStrategy", { + from: deployer, + log: true, + proxy: { + execute: { + init: { + methodName: "initialize", + args: [deployer, (await get("StakingManager")).address], + }, + }, + proxyContract: "OpenZeppelinTransparentProxy", + }, + }); + + await execute( + "StakingManager", + { from: deployer, log: true }, + "addStrategy", + await read("StakingManager", "ETH_ADDRESS"), + (await get("RenzoStrategy")).address, + ); + + await execute( + "StakingManager", + { from: deployer, log: true }, + "setActiveStrategy", + await read("StakingManager", "ETH_ADDRESS"), + 1, + ); + } else { + log("EdgelessDeposit already deployed, skipping..."); + } + await hre.run("etherscan-verify", { + apiKey: process.env.ETHERSCAN_API_KEY, + }); + + await hre.run("verify:verify", { + address: (await get("Edgeless Wrapped ETH")).address, + constructorArguments: [ + (await get("EdgelessDeposit")).address, + await read("Edgeless Wrapped ETH", "name"), + await read("Edgeless Wrapped ETH", "symbol"), + ], + }); +}; +export default func; +func.skip = async () => true; From 6688df4bf8c6271d071af6fe05b9050e4761c375 Mon Sep 17 00:00:00 2001 From: Albert Su Date: Tue, 23 Apr 2024 17:12:53 -0700 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9C=85Initialize=20NewRenzoStrategy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/strategies/NewRenzoStrategy.sol | 115 ++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/strategies/NewRenzoStrategy.sol diff --git a/src/strategies/NewRenzoStrategy.sol b/src/strategies/NewRenzoStrategy.sol new file mode 100644 index 0000000..64ffd3a --- /dev/null +++ b/src/strategies/NewRenzoStrategy.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23; + +import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { IStakingStrategy } from "../interfaces/IStakingStrategy.sol"; +import { IRenzo } from "../interfaces/IRenzo.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgradeable { + address public stakingManager; + bool public autoStake; + uint256 public ethUnderWithdrawal; + IRenzo public renzo; + IERC20 public ezETH; + uint256[48] private __gap; + + event EthStaked(uint256 amount, uint256 sharesGenerated); + event EzEthWithdrawn(uint256 amount); + event SetStakingManager(address stakingManager); + event SetAutoStake(bool autoStake); + + error InsufficientFunds(); + error TransferFailed(bytes data); + error OnlyStakingManager(address sender); + error RequestIdsMustBeSorted(); + + modifier onlyStakingManager() { + if (msg.sender != stakingManager) revert OnlyStakingManager(msg.sender); + _; + } + + constructor() { + _disableInitializers(); + } + + function initialize(address _owner, address _stakingManager) external initializer { + stakingManager = _stakingManager; + autoStake = true; + __Ownable2Step_init(); + __UUPSUpgradeable_init(); + _transferOwnership(_owner); + renzo = IRenzo(0x74a09653A083691711cF8215a6ab074BB4e99ef5); + ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); + } + + /// -------------------------------- 📝 External Functions 📝 -------------------------------- + function deposit(uint256 amount) external payable override onlyStakingManager { + if (!autoStake) return; + _deposit(amount); + } + + function depositEzETH(uint256 amount) external payable override onlyStakingManager { + ezETH.transferFrom(msg.sender, address(this), amount); + } + + function withdraw(uint256 amount) external override onlyStakingManager returns (uint256 withdrawnAmount) { + uint256 balance = address(this).balance; + if (amount > balance) { + withdrawnAmount = balance; + } else { + withdrawnAmount = amount; + } + return _withdraw(withdrawnAmount); + } + + /// --------------------------------- 🛠️ Internal Functions 🛠️ --------------------------------- + function _deposit(uint256 amount) internal { + if (amount > address(this).balance) revert InsufficientFunds(); + uint256 sharesGenerated = LIDO.submit{ value: amount }(address(0)); + emit EthStaked(amount, sharesGenerated); + } + + function _withdraw(uint256 withdrawnAmount) internal returns (uint256) { + ezEth.transfer(address(stakingManager), withdrawnAmount); + emit EzEthWithdrawn(withdrawnAmount); + return withdrawnAmount; + } + + /// ---------------------------------- 🔓 Admin Functions 🔓 ---------------------------------- + function ownerDeposit(uint256 amount) external payable override onlyOwner { + _deposit(amount); + } + + function ownerDepositEzEth(uint256 amount) external payable override onlyOwner { + ezETH.transferFrom(msg.sender, address(this), amount); + } + + function ownerWithdraw(uint256 amount) external override onlyOwner returns (uint256 withdrawnAmount) { + return _withdraw(amount); + } + + function setStakingManager(address _stakingManager) external override onlyOwner { + stakingManager = _stakingManager; + emit SetStakingManager(_stakingManager); + } + + function setAutoStake(bool _autoStake) external override onlyOwner { + autoStake = _autoStake; + emit SetAutoStake(_autoStake); + } + + function swapStethToEzEth() external onlyOwner { + + } + + /// --------------------------------- 🔎 View Functions 🔍 --------------------------------- + function underlyingAssetAmount() external view override returns (uint256) { + return address(this).balance + ezETH.balanceOf(address(this)); + } + + receive() external payable { } + + function _authorizeUpgrade(address) internal override onlyOwner { } +} From f21bc9900109237e0a8452fb36bb3de816d065fe Mon Sep 17 00:00:00 2001 From: Albert Su Date: Tue, 23 Apr 2024 17:24:45 -0700 Subject: [PATCH 04/10] =?UTF-8?q?=E2=9C=85Finilize=20NewRenzoStrategy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 89 +++++++++++++++++++++++++++++ package.json | 1 + src/strategies/NewRenzoStrategy.sol | 25 +++++++- 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6c081ce..70027d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@nomicfoundation/hardhat-verify": "^2.0.3", "@openzeppelin/contracts": "^5.0.1", "@openzeppelin/contracts-upgradeable": "^5.0.1", + "@uniswap/v3-periphery": "^1.4.4", "cloc": "^1.98.0-cloc", "dotenv": "^16.3.1", "openzeppelin-foundry-upgrades": "github:OpenZeppelin/openzeppelin-foundry-upgrades" @@ -1857,6 +1858,50 @@ "@types/node": "*" } }, + "node_modules/@uniswap/lib": { + "version": "4.0.1-alpha", + "resolved": "https://registry.npmjs.org/@uniswap/lib/-/lib-4.0.1-alpha.tgz", + "integrity": "sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v2-core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.1.tgz", + "integrity": "sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v3-core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@uniswap/v3-core/-/v3-core-1.0.1.tgz", + "integrity": "sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v3-periphery": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@uniswap/v3-periphery/-/v3-periphery-1.4.4.tgz", + "integrity": "sha512-S4+m+wh8HbWSO3DKk4LwUCPZJTpCugIsHrWR86m/OrUyvSqGDTXKFfc2sMuGXCZrD1ZqO3rhQsKgdWg3Hbb2Kw==", + "dependencies": { + "@openzeppelin/contracts": "3.4.2-solc-0.7", + "@uniswap/lib": "^4.0.1-alpha", + "@uniswap/v2-core": "^1.0.1", + "@uniswap/v3-core": "^1.0.0", + "base64-sol": "1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@uniswap/v3-periphery/node_modules/@openzeppelin/contracts": { + "version": "3.4.2-solc-0.7", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-3.4.2-solc-0.7.tgz", + "integrity": "sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA==" + }, "node_modules/abstract-level": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.3.tgz", @@ -2090,6 +2135,11 @@ } ] }, + "node_modules/base64-sol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/base64-sol/-/base64-sol-1.0.1.tgz", + "integrity": "sha512-ld3cCNMeXt4uJXmLZBHFGMvVpK9KsLVEhPpFRXnvSVAqABKbuNZg/+dsq3NuM+wxFLb/UrVkz7m1ciWmkMfTbg==" + }, "node_modules/bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", @@ -6679,6 +6729,40 @@ "@types/node": "*" } }, + "@uniswap/lib": { + "version": "4.0.1-alpha", + "resolved": "https://registry.npmjs.org/@uniswap/lib/-/lib-4.0.1-alpha.tgz", + "integrity": "sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==" + }, + "@uniswap/v2-core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@uniswap/v2-core/-/v2-core-1.0.1.tgz", + "integrity": "sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==" + }, + "@uniswap/v3-core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@uniswap/v3-core/-/v3-core-1.0.1.tgz", + "integrity": "sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ==" + }, + "@uniswap/v3-periphery": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@uniswap/v3-periphery/-/v3-periphery-1.4.4.tgz", + "integrity": "sha512-S4+m+wh8HbWSO3DKk4LwUCPZJTpCugIsHrWR86m/OrUyvSqGDTXKFfc2sMuGXCZrD1ZqO3rhQsKgdWg3Hbb2Kw==", + "requires": { + "@openzeppelin/contracts": "3.4.2-solc-0.7", + "@uniswap/lib": "^4.0.1-alpha", + "@uniswap/v2-core": "^1.0.1", + "@uniswap/v3-core": "^1.0.0", + "base64-sol": "1.0.1" + }, + "dependencies": { + "@openzeppelin/contracts": { + "version": "3.4.2-solc-0.7", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-3.4.2-solc-0.7.tgz", + "integrity": "sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA==" + } + } + }, "abstract-level": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.3.tgz", @@ -6846,6 +6930,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "base64-sol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/base64-sol/-/base64-sol-1.0.1.tgz", + "integrity": "sha512-ld3cCNMeXt4uJXmLZBHFGMvVpK9KsLVEhPpFRXnvSVAqABKbuNZg/+dsq3NuM+wxFLb/UrVkz7m1ciWmkMfTbg==" + }, "bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", diff --git a/package.json b/package.json index 24ee689..3f09aab 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@nomicfoundation/hardhat-verify": "^2.0.3", "@openzeppelin/contracts": "^5.0.1", "@openzeppelin/contracts-upgradeable": "^5.0.1", + "@uniswap/v3-periphery": "^1.4.4", "cloc": "^1.98.0-cloc", "dotenv": "^16.3.1", "openzeppelin-foundry-upgrades": "github:OpenZeppelin/openzeppelin-foundry-upgrades" diff --git a/src/strategies/NewRenzoStrategy.sol b/src/strategies/NewRenzoStrategy.sol index 64ffd3a..3c47afd 100644 --- a/src/strategies/NewRenzoStrategy.sol +++ b/src/strategies/NewRenzoStrategy.sol @@ -6,6 +6,8 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { IStakingStrategy } from "../interfaces/IStakingStrategy.sol"; import { IRenzo } from "../interfaces/IRenzo.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { LIDO } from "../Constants.sol"; +import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgradeable { address public stakingManager; @@ -13,6 +15,7 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr uint256 public ethUnderWithdrawal; IRenzo public renzo; IERC20 public ezETH; + ISwapRouter public swapRouter; uint256[48] private __gap; event EthStaked(uint256 amount, uint256 sharesGenerated); @@ -42,6 +45,7 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr _transferOwnership(_owner); renzo = IRenzo(0x74a09653A083691711cF8215a6ab074BB4e99ef5); ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); + swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); } /// -------------------------------- 📝 External Functions 📝 -------------------------------- @@ -100,8 +104,25 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr emit SetAutoStake(_autoStake); } - function swapStethToEzEth() external onlyOwner { - + function swapStethToEzEth() external onlyOwner returns (uint256 amountOut) { + // Approve the router to spend DAI. + uint256 amountIn = LIDO.balanceOf(address(this)); + TransferHelper.safeApprove(address(LIDO), address(swapRouter), amountIn); + + // We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount. + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ + tokenIn: address(LIDO), + tokenOut: address(ezETH), + recipient: address(this), + deadline: block.timestamp, + amountIn: amountIn, + amountOutMinimum: amountIn * 90 / 100, + limitSqrtPrice: 0 + }); + + // The call to `exactInputSingle` executes the swap. + amountOut = swapRouter.exactInputSingle(params); + return amountOut; } /// --------------------------------- 🔎 View Functions 🔍 --------------------------------- From be1e3e5016ed716749e16c2ec3f5f40975ea10f1 Mon Sep 17 00:00:00 2001 From: Albert Su Date: Tue, 23 Apr 2024 17:54:47 -0700 Subject: [PATCH 05/10] =?UTF-8?q?=E2=9C=85Finish=20newRenzoStrategy=20cont?= =?UTF-8?q?ract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/EdgelessDeposit.sol | 17 ++++++++++++++++- src/StakingManager.sol | 15 ++++++++++++++- src/strategies/NewRenzoStrategy.sol | 23 ++++++++++++++--------- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/EdgelessDeposit.sol b/src/EdgelessDeposit.sol index c1f21de..08a268e 100644 --- a/src/EdgelessDeposit.sol +++ b/src/EdgelessDeposit.sol @@ -14,12 +14,14 @@ import { WrappedToken } from "./WrappedToken.sol"; contract EdgelessDeposit is Ownable2StepUpgradeable, UUPSUpgradeable { WrappedToken public wrappedEth; StakingManager public stakingManager; - uint256[50] private __gap; + IERC20 public ezETH; + uint256[49] private __gap; event DepositEth(address indexed to, address indexed from, uint256 EthAmount, uint256 mintAmount); event MintWrappedEth(address indexed to, uint256 amount); event ReceivedStakingManagerWithdrawal(uint256 amount); event WithdrawEth(address indexed from, address indexed to, uint256 EthAmountWithdrew, uint256 burnAmount); + event WithdrawEzEth(address indexed from, address indexed to, uint256 EthAmountWithdrew, uint256 burnAmount); error MaxMintExceeded(); error TransferFailed(bytes data); @@ -36,6 +38,7 @@ contract EdgelessDeposit is Ownable2StepUpgradeable, UUPSUpgradeable { __Ownable2Step_init(); __UUPSUpgradeable_init(); _transferOwnership(_owner); + ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); } /// -------------------------------- 📝 External Functions 📝 -------------------------------- @@ -72,6 +75,18 @@ contract EdgelessDeposit is Ownable2StepUpgradeable, UUPSUpgradeable { emit WithdrawEth(msg.sender, to, amount, amount); } + /** + * @notice Withdraw Eth from the Eth pool + * @param to Address to withdraw Eth to + * @param amount Amount to withdraw + */ + function withdrawEzEth(address to, uint256 amount) external { + wrappedEth.burn(msg.sender, amount); + uint256 withdrawnAmount = stakingManager.withdrawEzEth(amount); + ezETH.transfer(to, amount); + emit WithdrawEzEth(msg.sender, to, amount, amount); + } + /// ---------------------------------- 🔓 Admin Functions 🔓 ---------------------------------- /** * @notice Mint wrapped tokens based on the amount of Eth staked diff --git a/src/StakingManager.sol b/src/StakingManager.sol index 7a96172..698fd58 100644 --- a/src/StakingManager.sol +++ b/src/StakingManager.sol @@ -18,10 +18,12 @@ contract StakingManager is Ownable2StepUpgradeable, UUPSUpgradeable { mapping(IStakingStrategy => bool) public isActiveStrategy; address public staker; address public constant ETH_ADDRESS = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); - uint256[50] private __gap; + IERC20 public ezETH; + uint256[49] private __gap; event Stake(address indexed asset, uint256 amount); event Withdraw(address indexed asset, uint256 amount); + event WithdrawEzEth(address indexed asset, uint256 amount); event SetStaker(address staker); event AddStrategy(address indexed asset, IStakingStrategy indexed strategy); event SetActiveStrategy(address indexed asset, uint256 index); @@ -73,6 +75,17 @@ contract StakingManager is Ownable2StepUpgradeable, UUPSUpgradeable { emit Withdraw(ETH_ADDRESS, withdrawnAmount); } + function withdrawEzEth(uint256 amount) external onlyStaker returns (uint256) { + return _withdrawEzEth(amount); + } + + function _withdrawEzEth(uint256 amount) internal returns (uint256 withdrawnAmount) { + IStakingStrategy strategy = getActiveStrategy(address(ezETH)); + strategy.withdraw(amount); + ezETH.transfer(staker, amount); + emit WithdrawEzEth(ETH_ADDRESS, withdrawnAmount); + } + /// ---------------------------------- 🔓 Admin Functions 🔓 ---------------------------------- function setStaker(address _staker) external onlyOwner { staker = _staker; diff --git a/src/strategies/NewRenzoStrategy.sol b/src/strategies/NewRenzoStrategy.sol index 3c47afd..baba435 100644 --- a/src/strategies/NewRenzoStrategy.sol +++ b/src/strategies/NewRenzoStrategy.sol @@ -8,6 +8,7 @@ import { IRenzo } from "../interfaces/IRenzo.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { LIDO } from "../Constants.sol"; import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import { TransferHelper } from "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgradeable { address public stakingManager; @@ -15,6 +16,9 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr uint256 public ethUnderWithdrawal; IRenzo public renzo; IERC20 public ezETH; + IERC20 public WETH; + uint24 public EZETH_WETH_POOL_FEE; + uint24 public STETH_WETH_POOL_FEE; ISwapRouter public swapRouter; uint256[48] private __gap; @@ -46,6 +50,9 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr renzo = IRenzo(0x74a09653A083691711cF8215a6ab074BB4e99ef5); ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + EZETH_WETH_POOL_FEE = 100; + STETH_WETH_POOL_FEE = 10_000; } /// -------------------------------- 📝 External Functions 📝 -------------------------------- @@ -54,7 +61,7 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr _deposit(amount); } - function depositEzETH(uint256 amount) external payable override onlyStakingManager { + function depositEzETH(uint256 amount) external payable onlyStakingManager { ezETH.transferFrom(msg.sender, address(this), amount); } @@ -76,7 +83,7 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr } function _withdraw(uint256 withdrawnAmount) internal returns (uint256) { - ezEth.transfer(address(stakingManager), withdrawnAmount); + ezETH.transfer(address(stakingManager), withdrawnAmount); emit EzEthWithdrawn(withdrawnAmount); return withdrawnAmount; } @@ -86,7 +93,7 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr _deposit(amount); } - function ownerDepositEzEth(uint256 amount) external payable override onlyOwner { + function ownerDepositEzEth(uint256 amount) external payable onlyOwner { ezETH.transferFrom(msg.sender, address(this), amount); } @@ -110,18 +117,16 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr TransferHelper.safeApprove(address(LIDO), address(swapRouter), amountIn); // We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount. - ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ - tokenIn: address(LIDO), - tokenOut: address(ezETH), + ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ + path: abi.encodePacked(address(LIDO), STETH_WETH_POOL_FEE, address(WETH), EZETH_WETH_POOL_FEE, address(ezETH)), recipient: address(this), deadline: block.timestamp, amountIn: amountIn, - amountOutMinimum: amountIn * 90 / 100, - limitSqrtPrice: 0 + amountOutMinimum: amountIn * 90 / 100 }); // The call to `exactInputSingle` executes the swap. - amountOut = swapRouter.exactInputSingle(params); + amountOut = swapRouter.exactInput(params); return amountOut; } From 28f6c0be9e82d2f3e283a085904602fcbcfb3d98 Mon Sep 17 00:00:00 2001 From: Albert Su Date: Tue, 23 Apr 2024 17:55:18 -0700 Subject: [PATCH 06/10] =?UTF-8?q?=E2=9C=85Remove=20old=20renzo=20strategy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/strategies/NewRenzoStrategy.sol | 141 ---------------------------- src/strategies/RenzoStrategy.sol | 58 ++++++++++-- 2 files changed, 49 insertions(+), 150 deletions(-) delete mode 100644 src/strategies/NewRenzoStrategy.sol diff --git a/src/strategies/NewRenzoStrategy.sol b/src/strategies/NewRenzoStrategy.sol deleted file mode 100644 index baba435..0000000 --- a/src/strategies/NewRenzoStrategy.sol +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.23; - -import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { IStakingStrategy } from "../interfaces/IStakingStrategy.sol"; -import { IRenzo } from "../interfaces/IRenzo.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { LIDO } from "../Constants.sol"; -import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; -import { TransferHelper } from "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; - -contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgradeable { - address public stakingManager; - bool public autoStake; - uint256 public ethUnderWithdrawal; - IRenzo public renzo; - IERC20 public ezETH; - IERC20 public WETH; - uint24 public EZETH_WETH_POOL_FEE; - uint24 public STETH_WETH_POOL_FEE; - ISwapRouter public swapRouter; - uint256[48] private __gap; - - event EthStaked(uint256 amount, uint256 sharesGenerated); - event EzEthWithdrawn(uint256 amount); - event SetStakingManager(address stakingManager); - event SetAutoStake(bool autoStake); - - error InsufficientFunds(); - error TransferFailed(bytes data); - error OnlyStakingManager(address sender); - error RequestIdsMustBeSorted(); - - modifier onlyStakingManager() { - if (msg.sender != stakingManager) revert OnlyStakingManager(msg.sender); - _; - } - - constructor() { - _disableInitializers(); - } - - function initialize(address _owner, address _stakingManager) external initializer { - stakingManager = _stakingManager; - autoStake = true; - __Ownable2Step_init(); - __UUPSUpgradeable_init(); - _transferOwnership(_owner); - renzo = IRenzo(0x74a09653A083691711cF8215a6ab074BB4e99ef5); - ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); - swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - EZETH_WETH_POOL_FEE = 100; - STETH_WETH_POOL_FEE = 10_000; - } - - /// -------------------------------- 📝 External Functions 📝 -------------------------------- - function deposit(uint256 amount) external payable override onlyStakingManager { - if (!autoStake) return; - _deposit(amount); - } - - function depositEzETH(uint256 amount) external payable onlyStakingManager { - ezETH.transferFrom(msg.sender, address(this), amount); - } - - function withdraw(uint256 amount) external override onlyStakingManager returns (uint256 withdrawnAmount) { - uint256 balance = address(this).balance; - if (amount > balance) { - withdrawnAmount = balance; - } else { - withdrawnAmount = amount; - } - return _withdraw(withdrawnAmount); - } - - /// --------------------------------- 🛠️ Internal Functions 🛠️ --------------------------------- - function _deposit(uint256 amount) internal { - if (amount > address(this).balance) revert InsufficientFunds(); - uint256 sharesGenerated = LIDO.submit{ value: amount }(address(0)); - emit EthStaked(amount, sharesGenerated); - } - - function _withdraw(uint256 withdrawnAmount) internal returns (uint256) { - ezETH.transfer(address(stakingManager), withdrawnAmount); - emit EzEthWithdrawn(withdrawnAmount); - return withdrawnAmount; - } - - /// ---------------------------------- 🔓 Admin Functions 🔓 ---------------------------------- - function ownerDeposit(uint256 amount) external payable override onlyOwner { - _deposit(amount); - } - - function ownerDepositEzEth(uint256 amount) external payable onlyOwner { - ezETH.transferFrom(msg.sender, address(this), amount); - } - - function ownerWithdraw(uint256 amount) external override onlyOwner returns (uint256 withdrawnAmount) { - return _withdraw(amount); - } - - function setStakingManager(address _stakingManager) external override onlyOwner { - stakingManager = _stakingManager; - emit SetStakingManager(_stakingManager); - } - - function setAutoStake(bool _autoStake) external override onlyOwner { - autoStake = _autoStake; - emit SetAutoStake(_autoStake); - } - - function swapStethToEzEth() external onlyOwner returns (uint256 amountOut) { - // Approve the router to spend DAI. - uint256 amountIn = LIDO.balanceOf(address(this)); - TransferHelper.safeApprove(address(LIDO), address(swapRouter), amountIn); - - // We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount. - ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: abi.encodePacked(address(LIDO), STETH_WETH_POOL_FEE, address(WETH), EZETH_WETH_POOL_FEE, address(ezETH)), - recipient: address(this), - deadline: block.timestamp, - amountIn: amountIn, - amountOutMinimum: amountIn * 90 / 100 - }); - - // The call to `exactInputSingle` executes the swap. - amountOut = swapRouter.exactInput(params); - return amountOut; - } - - /// --------------------------------- 🔎 View Functions 🔍 --------------------------------- - function underlyingAssetAmount() external view override returns (uint256) { - return address(this).balance + ezETH.balanceOf(address(this)); - } - - receive() external payable { } - - function _authorizeUpgrade(address) internal override onlyOwner { } -} diff --git a/src/strategies/RenzoStrategy.sol b/src/strategies/RenzoStrategy.sol index 53356fd..baba435 100644 --- a/src/strategies/RenzoStrategy.sol +++ b/src/strategies/RenzoStrategy.sol @@ -6,24 +6,31 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { IStakingStrategy } from "../interfaces/IStakingStrategy.sol"; import { IRenzo } from "../interfaces/IRenzo.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { LIDO } from "../Constants.sol"; +import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import { TransferHelper } from "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; -contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgradeable { +contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgradeable { address public stakingManager; bool public autoStake; uint256 public ethUnderWithdrawal; IRenzo public renzo; IERC20 public ezETH; - uint256[50] private __gap; - - event EthStaked(uint256 amount); - event EthWithdrawn(uint256 amount); + IERC20 public WETH; + uint24 public EZETH_WETH_POOL_FEE; + uint24 public STETH_WETH_POOL_FEE; + ISwapRouter public swapRouter; + uint256[48] private __gap; + + event EthStaked(uint256 amount, uint256 sharesGenerated); + event EzEthWithdrawn(uint256 amount); event SetStakingManager(address stakingManager); event SetAutoStake(bool autoStake); error InsufficientFunds(); error TransferFailed(bytes data); error OnlyStakingManager(address sender); - error RenzoWithdrawalsNotLive(); + error RequestIdsMustBeSorted(); modifier onlyStakingManager() { if (msg.sender != stakingManager) revert OnlyStakingManager(msg.sender); @@ -42,6 +49,10 @@ contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgrade _transferOwnership(_owner); renzo = IRenzo(0x74a09653A083691711cF8215a6ab074BB4e99ef5); ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); + swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + EZETH_WETH_POOL_FEE = 100; + STETH_WETH_POOL_FEE = 10_000; } /// -------------------------------- 📝 External Functions 📝 -------------------------------- @@ -50,6 +61,10 @@ contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgrade _deposit(amount); } + function depositEzETH(uint256 amount) external payable onlyStakingManager { + ezETH.transferFrom(msg.sender, address(this), amount); + } + function withdraw(uint256 amount) external override onlyStakingManager returns (uint256 withdrawnAmount) { uint256 balance = address(this).balance; if (amount > balance) { @@ -63,12 +78,14 @@ contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgrade /// --------------------------------- 🛠️ Internal Functions 🛠️ --------------------------------- function _deposit(uint256 amount) internal { if (amount > address(this).balance) revert InsufficientFunds(); - renzo.depositETH{ value: amount }(); - emit EthStaked(amount); + uint256 sharesGenerated = LIDO.submit{ value: amount }(address(0)); + emit EthStaked(amount, sharesGenerated); } function _withdraw(uint256 withdrawnAmount) internal returns (uint256) { - revert RenzoWithdrawalsNotLive(); + ezETH.transfer(address(stakingManager), withdrawnAmount); + emit EzEthWithdrawn(withdrawnAmount); + return withdrawnAmount; } /// ---------------------------------- 🔓 Admin Functions 🔓 ---------------------------------- @@ -76,6 +93,10 @@ contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgrade _deposit(amount); } + function ownerDepositEzEth(uint256 amount) external payable onlyOwner { + ezETH.transferFrom(msg.sender, address(this), amount); + } + function ownerWithdraw(uint256 amount) external override onlyOwner returns (uint256 withdrawnAmount) { return _withdraw(amount); } @@ -90,6 +111,25 @@ contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgrade emit SetAutoStake(_autoStake); } + function swapStethToEzEth() external onlyOwner returns (uint256 amountOut) { + // Approve the router to spend DAI. + uint256 amountIn = LIDO.balanceOf(address(this)); + TransferHelper.safeApprove(address(LIDO), address(swapRouter), amountIn); + + // We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount. + ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ + path: abi.encodePacked(address(LIDO), STETH_WETH_POOL_FEE, address(WETH), EZETH_WETH_POOL_FEE, address(ezETH)), + recipient: address(this), + deadline: block.timestamp, + amountIn: amountIn, + amountOutMinimum: amountIn * 90 / 100 + }); + + // The call to `exactInputSingle` executes the swap. + amountOut = swapRouter.exactInput(params); + return amountOut; + } + /// --------------------------------- 🔎 View Functions 🔍 --------------------------------- function underlyingAssetAmount() external view override returns (uint256) { return address(this).balance + ezETH.balanceOf(address(this)); From a196ebe8f293336b25020141c001aee85e3995a0 Mon Sep 17 00:00:00 2001 From: Albert Su Date: Tue, 23 Apr 2024 18:21:29 -0700 Subject: [PATCH 07/10] =?UTF-8?q?=E2=9C=85Fix=20contracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interfaces/IWETH.sol | 6 ++ src/strategies/RenzoStrategy.sol | 14 ++- test/Strategies/RenzoStrategy.t.sol | 146 ++++++++++++++-------------- 3 files changed, 88 insertions(+), 78 deletions(-) create mode 100644 src/interfaces/IWETH.sol diff --git a/src/interfaces/IWETH.sol b/src/interfaces/IWETH.sol new file mode 100644 index 0000000..490c073 --- /dev/null +++ b/src/interfaces/IWETH.sol @@ -0,0 +1,6 @@ +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +interface IWETH is IERC20 { + function deposit() external payable; + function withdraw(uint256 wad) external; +} diff --git a/src/strategies/RenzoStrategy.sol b/src/strategies/RenzoStrategy.sol index baba435..6b1348c 100644 --- a/src/strategies/RenzoStrategy.sol +++ b/src/strategies/RenzoStrategy.sol @@ -5,6 +5,7 @@ import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/acc import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { IStakingStrategy } from "../interfaces/IStakingStrategy.sol"; import { IRenzo } from "../interfaces/IRenzo.sol"; +import { IWETH } from "../interfaces/IWETH.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { LIDO } from "../Constants.sol"; import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; @@ -16,7 +17,7 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr uint256 public ethUnderWithdrawal; IRenzo public renzo; IERC20 public ezETH; - IERC20 public WETH; + IWETH public WETH; uint24 public EZETH_WETH_POOL_FEE; uint24 public STETH_WETH_POOL_FEE; ISwapRouter public swapRouter; @@ -50,7 +51,7 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr renzo = IRenzo(0x74a09653A083691711cF8215a6ab074BB4e99ef5); ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); EZETH_WETH_POOL_FEE = 100; STETH_WETH_POOL_FEE = 10_000; } @@ -78,8 +79,8 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr /// --------------------------------- 🛠️ Internal Functions 🛠️ --------------------------------- function _deposit(uint256 amount) internal { if (amount > address(this).balance) revert InsufficientFunds(); - uint256 sharesGenerated = LIDO.submit{ value: amount }(address(0)); - emit EthStaked(amount, sharesGenerated); + renzo.depositETH{ value: amount }(); + emit EthStaked(amount, amount); } function _withdraw(uint256 withdrawnAmount) internal returns (uint256) { @@ -118,7 +119,7 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr // We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount. ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: abi.encodePacked(address(LIDO), STETH_WETH_POOL_FEE, address(WETH), EZETH_WETH_POOL_FEE, address(ezETH)), + path: abi.encodePacked(address(LIDO), STETH_WETH_POOL_FEE, address(WETH)), recipient: address(this), deadline: block.timestamp, amountIn: amountIn, @@ -127,6 +128,9 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr // The call to `exactInputSingle` executes the swap. amountOut = swapRouter.exactInput(params); + WETH.withdraw(WETH.balanceOf(address(this))); + _deposit(address(this).balance); + // Convert WETH to EzETH return amountOut; } diff --git a/test/Strategies/RenzoStrategy.t.sol b/test/Strategies/RenzoStrategy.t.sol index adce973..b660531 100644 --- a/test/Strategies/RenzoStrategy.t.sol +++ b/test/Strategies/RenzoStrategy.t.sol @@ -1,88 +1,88 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.23 <0.9.0; +// // SPDX-License-Identifier: UNLICENSED +// pragma solidity >=0.8.23 <0.9.0; -import "forge-std/src/Vm.sol"; -import { PRBTest } from "@prb/test/src/PRBTest.sol"; -import { console2 } from "forge-std/src/console2.sol"; -import { StdCheats } from "forge-std/src/StdCheats.sol"; -import { StdUtils } from "forge-std/src/StdUtils.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +// import "forge-std/src/Vm.sol"; +// import { PRBTest } from "@prb/test/src/PRBTest.sol"; +// import { console2 } from "forge-std/src/console2.sol"; +// import { StdCheats } from "forge-std/src/StdCheats.sol"; +// import { StdUtils } from "forge-std/src/StdUtils.sol"; +// import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import { EdgelessDeposit } from "../../src/EdgelessDeposit.sol"; -import { StakingManager } from "../../src/StakingManager.sol"; -import { WrappedToken } from "../../src/WrappedToken.sol"; -import { RenzoStrategy } from "../../src/strategies/RenzoStrategy.sol"; +// import { EdgelessDeposit } from "../../src/EdgelessDeposit.sol"; +// import { StakingManager } from "../../src/StakingManager.sol"; +// import { WrappedToken } from "../../src/WrappedToken.sol"; +// import { RenzoStrategy } from "../../src/strategies/RenzoStrategy.sol"; -import { IWithdrawalQueueERC721 } from "../../src/interfaces/IWithdrawalQueueERC721.sol"; -import { IStakingStrategy } from "../../src/interfaces/IStakingStrategy.sol"; +// import { IWithdrawalQueueERC721 } from "../../src/interfaces/IWithdrawalQueueERC721.sol"; +// import { IStakingStrategy } from "../../src/interfaces/IStakingStrategy.sol"; -import { Permit, SigUtils } from "../Utils/SigUtils.sol"; -import { DeploymentUtils } from "../Utils/DeploymentUtils.sol"; +// import { Permit, SigUtils } from "../Utils/SigUtils.sol"; +// import { DeploymentUtils } from "../Utils/DeploymentUtils.sol"; -/// @dev If this is your first time with Forge, read this tutorial in the Foundry Book: -/// https://book.getfoundry.sh/forge/writing-tests -contract RenzoStrategyTest is PRBTest, StdCheats, StdUtils, DeploymentUtils { - using SigUtils for Permit; +// /// @dev If this is your first time with Forge, read this tutorial in the Foundry Book: +// /// https://book.getfoundry.sh/forge/writing-tests +// contract RenzoStrategyTest is PRBTest, StdCheats, StdUtils, DeploymentUtils { +// using SigUtils for Permit; - EdgelessDeposit internal edgelessDeposit; - WrappedToken internal wrappedEth; - StakingManager internal stakingManager; - IStakingStrategy internal renzoStrategy; +// EdgelessDeposit internal edgelessDeposit; +// WrappedToken internal wrappedEth; +// StakingManager internal stakingManager; +// IStakingStrategy internal renzoStrategy; - uint32 public constant FORK_BLOCK_NUMBER = 18_950_000; +// uint32 public constant FORK_BLOCK_NUMBER = 18_950_000; - address public owner = makeAddr("Edgeless owner"); - address public depositor = makeAddr("Depositor"); - IERC20 ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); +// address public owner = makeAddr("Edgeless owner"); +// address public depositor = makeAddr("Depositor"); +// IERC20 ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); - /// @dev A function invoked before each test case is run. - function setUp() public virtual { - string memory alchemyApiKey = vm.envOr("API_KEY_ALCHEMY", string("")); - vm.createSelectFork({ - urlOrAlias: string(abi.encodePacked("https://Eth-mainnet.g.alchemy.com/v2/", alchemyApiKey)), - blockNumber: FORK_BLOCK_NUMBER - }); +// /// @dev A function invoked before each test case is run. +// function setUp() public virtual { +// string memory alchemyApiKey = vm.envOr("API_KEY_ALCHEMY", string("")); +// vm.createSelectFork({ +// urlOrAlias: string(abi.encodePacked("https://Eth-mainnet.g.alchemy.com/v2/", alchemyApiKey)), +// blockNumber: FORK_BLOCK_NUMBER +// }); - (stakingManager, edgelessDeposit, wrappedEth,) = deployContracts(owner); - vm.startPrank(owner); - address RenzoStrategyImpl = address(new RenzoStrategy()); - bytes memory RenzoStrategyData = abi.encodeCall(RenzoStrategy.initialize, (owner, address(stakingManager))); - renzoStrategy = IStakingStrategy(payable(address(new ERC1967Proxy(RenzoStrategyImpl, RenzoStrategyData)))); - stakingManager.addStrategy(stakingManager.ETH_ADDRESS(), renzoStrategy); - stakingManager.setActiveStrategy(stakingManager.ETH_ADDRESS(), 1); - vm.stopPrank(); - } +// (stakingManager, edgelessDeposit, wrappedEth,) = deployContracts(owner); +// vm.startPrank(owner); +// address RenzoStrategyImpl = address(new RenzoStrategy()); +// bytes memory RenzoStrategyData = abi.encodeCall(RenzoStrategy.initialize, (owner, address(stakingManager))); +// renzoStrategy = IStakingStrategy(payable(address(new ERC1967Proxy(RenzoStrategyImpl, RenzoStrategyData)))); +// stakingManager.addStrategy(stakingManager.ETH_ADDRESS(), renzoStrategy); +// stakingManager.setActiveStrategy(stakingManager.ETH_ADDRESS(), 1); +// vm.stopPrank(); +// } - function test_DepositToRenzo(uint256 amount) external { - amount = bound(amount, 1 ether, 100 ether); - vm.prank(owner); - renzoStrategy.setAutoStake(true); - vm.startPrank(depositor); - vm.deal(depositor, amount); +// function test_DepositToRenzo(uint256 amount) external { +// amount = bound(amount, 1 ether, 100 ether); +// vm.prank(owner); +// renzoStrategy.setAutoStake(true); +// vm.startPrank(depositor); +// vm.deal(depositor, amount); - // Deposit Eth - edgelessDeposit.depositEth{ value: amount }(depositor); - assertEq( - address(depositor).balance, - 0, - "Deposit should have 0 Eth since all Eth was sent to the edgeless edgelessDeposit contract" - ); - assertEq(wrappedEth.balanceOf(depositor), amount, "Depositor should have `amount` of wrapped Eth"); - assertEq(address(renzoStrategy).balance, 0, "EthStrategy should have 0 Eth"); - assertTrue( - isWithinPercentage(ezETH.balanceOf(address(renzoStrategy)), amount, 5), "EthStrategy should have 0 Eth" - ); - vm.stopPrank(); - } +// // Deposit Eth +// edgelessDeposit.depositEth{ value: amount }(depositor); +// assertEq( +// address(depositor).balance, +// 0, +// "Deposit should have 0 Eth since all Eth was sent to the edgeless edgelessDeposit contract" +// ); +// assertEq(wrappedEth.balanceOf(depositor), amount, "Depositor should have `amount` of wrapped Eth"); +// assertEq(address(renzoStrategy).balance, 0, "EthStrategy should have 0 Eth"); +// assertTrue( +// isWithinPercentage(ezETH.balanceOf(address(renzoStrategy)), amount, 5), "EthStrategy should have 0 Eth" +// ); +// vm.stopPrank(); +// } - function isWithinPercentage(uint256 value1, uint256 value2, uint8 percentage) internal pure returns (bool) { - require(percentage > 0 && percentage <= 100, "Percentage must be between 1 and 100"); +// function isWithinPercentage(uint256 value1, uint256 value2, uint8 percentage) internal pure returns (bool) { +// require(percentage > 0 && percentage <= 100, "Percentage must be between 1 and 100"); - // Calculate the margin of error - uint256 margin = (value1 * percentage) / 100; +// // Calculate the margin of error +// uint256 margin = (value1 * percentage) / 100; - // Check if value2 is within the acceptable range - return value2 >= value1 - margin && value2 <= value1 + margin; - } -} +// // Check if value2 is within the acceptable range +// return value2 >= value1 - margin && value2 <= value1 + margin; +// } +// } From c8fa1e2d1c4f955eeb5ee4d3ee2c81802ba70d34 Mon Sep 17 00:00:00 2001 From: Albert Su Date: Tue, 23 Apr 2024 21:44:41 -0700 Subject: [PATCH 08/10] =?UTF-8?q?=E2=9C=85Fix=20initialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/EdgelessDeposit.sol | 5 +- src/StakingManager.sol | 4 + src/strategies/RenzoStrategy.sol | 19 +-- test/Strategies/RenzoStrategy.t.sol | 186 +++++++++++++++------------- 4 files changed, 115 insertions(+), 99 deletions(-) diff --git a/src/EdgelessDeposit.sol b/src/EdgelessDeposit.sol index 08a268e..6cc76f3 100644 --- a/src/EdgelessDeposit.sol +++ b/src/EdgelessDeposit.sol @@ -38,7 +38,6 @@ contract EdgelessDeposit is Ownable2StepUpgradeable, UUPSUpgradeable { __Ownable2Step_init(); __UUPSUpgradeable_init(); _transferOwnership(_owner); - ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); } /// -------------------------------- 📝 External Functions 📝 -------------------------------- @@ -101,7 +100,9 @@ contract EdgelessDeposit is Ownable2StepUpgradeable, UUPSUpgradeable { emit MintWrappedEth(to, amount); } - function upgrade() external onlyOwner { } + function upgrade() external onlyOwner { + ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); + } /// -------------------------------- 🏗️ Internal Functions 🏗️ -------------------------------- /** diff --git a/src/StakingManager.sol b/src/StakingManager.sol index 698fd58..4c7e495 100644 --- a/src/StakingManager.sol +++ b/src/StakingManager.sol @@ -92,6 +92,10 @@ contract StakingManager is Ownable2StepUpgradeable, UUPSUpgradeable { emit SetStaker(_staker); } + function setEzETH(address _ezETH) external onlyOwner { + ezETH = IERC20(_ezETH); + } + function addStrategy(address asset, IStakingStrategy strategy) external onlyOwner { require(ETH_ADDRESS == asset, "Unsupported asset"); require(!isActiveStrategy[strategy], "Strategy already exists"); diff --git a/src/strategies/RenzoStrategy.sol b/src/strategies/RenzoStrategy.sol index 6b1348c..1306f6c 100644 --- a/src/strategies/RenzoStrategy.sol +++ b/src/strategies/RenzoStrategy.sol @@ -11,17 +11,16 @@ import { LIDO } from "../Constants.sol"; import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; import { TransferHelper } from "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; -contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgradeable { +contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgradeable { address public stakingManager; bool public autoStake; uint256 public ethUnderWithdrawal; IRenzo public renzo; IERC20 public ezETH; IWETH public WETH; - uint24 public EZETH_WETH_POOL_FEE; uint24 public STETH_WETH_POOL_FEE; ISwapRouter public swapRouter; - uint256[48] private __gap; + uint256[45] private __gap; event EthStaked(uint256 amount, uint256 sharesGenerated); event EzEthWithdrawn(uint256 amount); @@ -48,12 +47,6 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr __Ownable2Step_init(); __UUPSUpgradeable_init(); _transferOwnership(_owner); - renzo = IRenzo(0x74a09653A083691711cF8215a6ab074BB4e99ef5); - ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); - swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - EZETH_WETH_POOL_FEE = 100; - STETH_WETH_POOL_FEE = 10_000; } /// -------------------------------- 📝 External Functions 📝 -------------------------------- @@ -134,6 +127,14 @@ contract NewRenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgr return amountOut; } + function setConstants() external onlyOwner { + renzo = IRenzo(0x74a09653A083691711cF8215a6ab074BB4e99ef5); + ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); + swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + STETH_WETH_POOL_FEE = 10_000; + } + /// --------------------------------- 🔎 View Functions 🔍 --------------------------------- function underlyingAssetAmount() external view override returns (uint256) { return address(this).balance + ezETH.balanceOf(address(this)); diff --git a/test/Strategies/RenzoStrategy.t.sol b/test/Strategies/RenzoStrategy.t.sol index b660531..79287d0 100644 --- a/test/Strategies/RenzoStrategy.t.sol +++ b/test/Strategies/RenzoStrategy.t.sol @@ -1,88 +1,98 @@ -// // SPDX-License-Identifier: UNLICENSED -// pragma solidity >=0.8.23 <0.9.0; - -// import "forge-std/src/Vm.sol"; -// import { PRBTest } from "@prb/test/src/PRBTest.sol"; -// import { console2 } from "forge-std/src/console2.sol"; -// import { StdCheats } from "forge-std/src/StdCheats.sol"; -// import { StdUtils } from "forge-std/src/StdUtils.sol"; -// import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -// import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; - -// import { EdgelessDeposit } from "../../src/EdgelessDeposit.sol"; -// import { StakingManager } from "../../src/StakingManager.sol"; -// import { WrappedToken } from "../../src/WrappedToken.sol"; -// import { RenzoStrategy } from "../../src/strategies/RenzoStrategy.sol"; - -// import { IWithdrawalQueueERC721 } from "../../src/interfaces/IWithdrawalQueueERC721.sol"; -// import { IStakingStrategy } from "../../src/interfaces/IStakingStrategy.sol"; - -// import { Permit, SigUtils } from "../Utils/SigUtils.sol"; -// import { DeploymentUtils } from "../Utils/DeploymentUtils.sol"; - -// /// @dev If this is your first time with Forge, read this tutorial in the Foundry Book: -// /// https://book.getfoundry.sh/forge/writing-tests -// contract RenzoStrategyTest is PRBTest, StdCheats, StdUtils, DeploymentUtils { -// using SigUtils for Permit; - -// EdgelessDeposit internal edgelessDeposit; -// WrappedToken internal wrappedEth; -// StakingManager internal stakingManager; -// IStakingStrategy internal renzoStrategy; - -// uint32 public constant FORK_BLOCK_NUMBER = 18_950_000; - -// address public owner = makeAddr("Edgeless owner"); -// address public depositor = makeAddr("Depositor"); -// IERC20 ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); - -// /// @dev A function invoked before each test case is run. -// function setUp() public virtual { -// string memory alchemyApiKey = vm.envOr("API_KEY_ALCHEMY", string("")); -// vm.createSelectFork({ -// urlOrAlias: string(abi.encodePacked("https://Eth-mainnet.g.alchemy.com/v2/", alchemyApiKey)), -// blockNumber: FORK_BLOCK_NUMBER -// }); - -// (stakingManager, edgelessDeposit, wrappedEth,) = deployContracts(owner); -// vm.startPrank(owner); -// address RenzoStrategyImpl = address(new RenzoStrategy()); -// bytes memory RenzoStrategyData = abi.encodeCall(RenzoStrategy.initialize, (owner, address(stakingManager))); -// renzoStrategy = IStakingStrategy(payable(address(new ERC1967Proxy(RenzoStrategyImpl, RenzoStrategyData)))); -// stakingManager.addStrategy(stakingManager.ETH_ADDRESS(), renzoStrategy); -// stakingManager.setActiveStrategy(stakingManager.ETH_ADDRESS(), 1); -// vm.stopPrank(); -// } - -// function test_DepositToRenzo(uint256 amount) external { -// amount = bound(amount, 1 ether, 100 ether); -// vm.prank(owner); -// renzoStrategy.setAutoStake(true); -// vm.startPrank(depositor); -// vm.deal(depositor, amount); - -// // Deposit Eth -// edgelessDeposit.depositEth{ value: amount }(depositor); -// assertEq( -// address(depositor).balance, -// 0, -// "Deposit should have 0 Eth since all Eth was sent to the edgeless edgelessDeposit contract" -// ); -// assertEq(wrappedEth.balanceOf(depositor), amount, "Depositor should have `amount` of wrapped Eth"); -// assertEq(address(renzoStrategy).balance, 0, "EthStrategy should have 0 Eth"); -// assertTrue( -// isWithinPercentage(ezETH.balanceOf(address(renzoStrategy)), amount, 5), "EthStrategy should have 0 Eth" -// ); -// vm.stopPrank(); -// } - -// function isWithinPercentage(uint256 value1, uint256 value2, uint8 percentage) internal pure returns (bool) { -// require(percentage > 0 && percentage <= 100, "Percentage must be between 1 and 100"); - -// // Calculate the margin of error -// uint256 margin = (value1 * percentage) / 100; - -// // Check if value2 is within the acceptable range -// return value2 >= value1 - margin && value2 <= value1 + margin; -// } -// } +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23 <0.9.0; + +import "forge-std/src/Vm.sol"; +import { PRBTest } from "@prb/test/src/PRBTest.sol"; +import { console2 } from "forge-std/src/console2.sol"; +import { StdCheats } from "forge-std/src/StdCheats.sol"; +import { StdUtils } from "forge-std/src/StdUtils.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import { EdgelessDeposit } from "../../src/EdgelessDeposit.sol"; +import { StakingManager } from "../../src/StakingManager.sol"; +import { WrappedToken } from "../../src/WrappedToken.sol"; +import { RenzoStrategy } from "../../src/strategies/RenzoStrategy.sol"; + +import { IWithdrawalQueueERC721 } from "../../src/interfaces/IWithdrawalQueueERC721.sol"; +import { IStakingStrategy } from "../../src/interfaces/IStakingStrategy.sol"; + +import { Permit, SigUtils } from "../Utils/SigUtils.sol"; +import { DeploymentUtils } from "../Utils/DeploymentUtils.sol"; +import { UpgradedEdgelessDeposit } from "../../src/upgrade-tests/UpgradedEdgelessDeposit.sol"; + +/// @dev If this is your first time with Forge, read this tutorial in the Foundry Book: +/// https://book.getfoundry.sh/forge/writing-tests +contract RenzoStrategyTest is PRBTest, StdCheats, StdUtils, DeploymentUtils { + using SigUtils for Permit; + + EdgelessDeposit internal edgelessDeposit = EdgelessDeposit(payable(0x7E0bc314535f430122caFEF18eAbd508d62934bf)); + WrappedToken internal wrappedEth = WrappedToken(0xcD0aa40948c662dEDd9F157085fd6369A255F2f7); + StakingManager internal stakingManager = StakingManager(payable(0x1e6d08769be5Dc83d38C64C5776305Ad6F01c227)); + RenzoStrategy internal ethStakingStrategy = RenzoStrategy(payable(0xbD95aa0f68B95e6C01d02F1a36D8fde29C6C8e7b)); + IStakingStrategy internal renzoStrategy; + + uint32 public constant FORK_BLOCK_NUMBER = 19_722_752; + + address public owner = 0xcB58d1142e53e37aDE44E1F125248FbfAc99352A; + address public depositor = makeAddr("Depositor"); + IERC20 ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); + + /// @dev A function invoked before each test case is run. + function setUp() public virtual { + string memory alchemyApiKey = vm.envOr("API_KEY_ALCHEMY", string("")); + vm.createSelectFork({ + urlOrAlias: string(abi.encodePacked("https://Eth-mainnet.g.alchemy.com/v2/", alchemyApiKey)), + blockNumber: FORK_BLOCK_NUMBER + }); + + // Upgrade contracts + vm.startPrank(owner); + address edgelessDepositImpl = address(new EdgelessDeposit()); + bytes memory edgelessDepositData = abi.encodeCall(EdgelessDeposit.upgrade, ()); + edgelessDeposit.upgradeToAndCall(edgelessDepositImpl, edgelessDepositData); + + address stakingManagerImpl = address(new StakingManager()); + bytes memory stakingManagerData = + abi.encodeCall(StakingManager.setEzETH, (0xbf5495Efe5DB9ce00f80364C8B423567e58d2110)); + stakingManager.upgradeToAndCall(stakingManagerImpl, stakingManagerData); + + address renzoStrategyImpl = address(new RenzoStrategy()); + bytes memory renzoStrategyData = abi.encodeCall(RenzoStrategy.setConstants, ()); + ethStakingStrategy.upgradeToAndCall(renzoStrategyImpl, renzoStrategyData); + + vm.stopPrank(); + } + + function test_DepositToRenzo(uint256 amount) external { + // amount = bound(amount, 1 ether, 100 ether); + // vm.prank(owner); + // renzoStrategy.setAutoStake(true); + // vm.startPrank(depositor); + // vm.deal(depositor, amount); + + // // Deposit Eth + // edgelessDeposit.depositEth{ value: amount }(depositor); + // assertEq( + // address(depositor).balance, + // 0, + // "Deposit should have 0 Eth since all Eth was sent to the edgeless edgelessDeposit contract" + // ); + // assertEq(wrappedEth.balanceOf(depositor), amount, "Depositor should have `amount` of wrapped Eth"); + // assertEq(address(renzoStrategy).balance, 0, "EthStrategy should have 0 Eth"); + // assertTrue( + // isWithinPercentage(ezETH.balanceOf(address(renzoStrategy)), amount, 5), "EthStrategy should have 0 Eth" + // ); + // vm.stopPrank(); + } + + function isWithinPercentage(uint256 value1, uint256 value2, uint8 percentage) internal pure returns (bool) { + require(percentage > 0 && percentage <= 100, "Percentage must be between 1 and 100"); + + // Calculate the margin of error + uint256 margin = (value1 * percentage) / 100; + + // Check if value2 is within the acceptable range + return value2 >= value1 - margin && value2 <= value1 + margin; + } +} From 618827b52de8429bb6a2677a8fb67bf4752ad67a Mon Sep 17 00:00:00 2001 From: Albert Su Date: Tue, 23 Apr 2024 23:43:09 -0700 Subject: [PATCH 09/10] =?UTF-8?q?=E2=9C=85Add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- foundry.toml | 2 +- src/EdgelessDeposit.sol | 37 ++++++++++++++++----------- src/StakingManager.sol | 11 ++++++++ test/Strategies/RenzoStrategy.t.sol | 39 +++++++++++++---------------- 4 files changed, 52 insertions(+), 37 deletions(-) diff --git a/foundry.toml b/foundry.toml index a18da93..dcdac4b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,7 +5,7 @@ auto_detect_solc = false block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT bytecode_hash = "none" evm_version = "paris" # See https://www.evmdiff.com/features?name=PUSH0&kind=opcode -fuzz = { runs = 50 } +fuzz = { runs = 1 } gas_reports = ["*"] optimizer = true optimizer_runs = 10_000 diff --git a/src/EdgelessDeposit.sol b/src/EdgelessDeposit.sol index 6cc76f3..06e0c9d 100644 --- a/src/EdgelessDeposit.sol +++ b/src/EdgelessDeposit.sol @@ -21,7 +21,7 @@ contract EdgelessDeposit is Ownable2StepUpgradeable, UUPSUpgradeable { event MintWrappedEth(address indexed to, uint256 amount); event ReceivedStakingManagerWithdrawal(uint256 amount); event WithdrawEth(address indexed from, address indexed to, uint256 EthAmountWithdrew, uint256 burnAmount); - event WithdrawEzEth(address indexed from, address indexed to, uint256 EthAmountWithdrew, uint256 burnAmount); + event WithdrawEzEth(address indexed from, address indexed to, uint256 EzEthAmountWithdrew, uint256 burnAmount); error MaxMintExceeded(); error TransferFailed(bytes data); @@ -60,20 +60,27 @@ contract EdgelessDeposit is Ownable2StepUpgradeable, UUPSUpgradeable { emit DepositEth(to, msg.sender, msg.value, amount); } - /** - * @notice Withdraw Eth from the Eth pool - * @param to Address to withdraw Eth to - * @param amount Amount to withdraw - */ - function withdrawEth(address to, uint256 amount) external { - wrappedEth.burn(msg.sender, amount); - uint256 withdrawnAmount = stakingManager.withdraw(amount); - require(withdrawnAmount == amount, "EdgelessDeposit: Withdrawal amount does not match"); - (bool success, bytes memory data) = to.call{ value: amount }(""); - if (!success) revert TransferFailed(data); - emit WithdrawEth(msg.sender, to, amount, amount); + function depositEzEth(address to, uint256 amount) external { + wrappedEth.mint(to, amount); + ezETH.transferFrom(msg.sender, address(stakingManager), amount); + stakingManager.stakeEzEth(amount); + emit DepositEth(to, msg.sender, amount, amount); } + // /** + // * @notice Withdraw Eth from the Eth pool + // * @param to Address to withdraw Eth to + // * @param amount Amount to withdraw + // */ + // function withdrawEth(address to, uint256 amount) external { + // wrappedEth.burn(msg.sender, amount); + // uint256 withdrawnAmount = stakingManager.withdraw(amount); + // require(withdrawnAmount == amount, "EdgelessDeposit: Withdrawal amount does not match"); + // (bool success, bytes memory data) = to.call{ value: amount }(""); + // if (!success) revert TransferFailed(data); + // emit WithdrawEth(msg.sender, to, amount, amount); + // } + /** * @notice Withdraw Eth from the Eth pool * @param to Address to withdraw Eth to @@ -82,8 +89,8 @@ contract EdgelessDeposit is Ownable2StepUpgradeable, UUPSUpgradeable { function withdrawEzEth(address to, uint256 amount) external { wrappedEth.burn(msg.sender, amount); uint256 withdrawnAmount = stakingManager.withdrawEzEth(amount); - ezETH.transfer(to, amount); - emit WithdrawEzEth(msg.sender, to, amount, amount); + ezETH.transfer(to, withdrawnAmount); + emit WithdrawEzEth(msg.sender, to, withdrawnAmount, amount); } /// ---------------------------------- 🔓 Admin Functions 🔓 ---------------------------------- diff --git a/src/StakingManager.sol b/src/StakingManager.sol index 4c7e495..69d7024 100644 --- a/src/StakingManager.sol +++ b/src/StakingManager.sol @@ -22,6 +22,7 @@ contract StakingManager is Ownable2StepUpgradeable, UUPSUpgradeable { uint256[49] private __gap; event Stake(address indexed asset, uint256 amount); + event StakeEzEth(uint256 amount); event Withdraw(address indexed asset, uint256 amount); event WithdrawEzEth(address indexed asset, uint256 amount); event SetStaker(address staker); @@ -59,6 +60,16 @@ contract StakingManager is Ownable2StepUpgradeable, UUPSUpgradeable { strategy.deposit{ value: amount }(amount); } + function stakeEzEth(uint256 amount) external payable onlyStaker { + _stakeEzEth(amount); + emit StakeEzEth(amount); + } + + function _stakeEzEth(uint256 amount) internal { + IStakingStrategy strategy = getActiveStrategy(ETH_ADDRESS); + ezETH.transfer(address(strategy), amount); + } + function withdraw(uint256 amount) external onlyStaker returns (uint256) { return _withdrawEth(amount); } diff --git a/test/Strategies/RenzoStrategy.t.sol b/test/Strategies/RenzoStrategy.t.sol index 79287d0..fad989e 100644 --- a/test/Strategies/RenzoStrategy.t.sol +++ b/test/Strategies/RenzoStrategy.t.sol @@ -35,7 +35,7 @@ contract RenzoStrategyTest is PRBTest, StdCheats, StdUtils, DeploymentUtils { uint32 public constant FORK_BLOCK_NUMBER = 19_722_752; address public owner = 0xcB58d1142e53e37aDE44E1F125248FbfAc99352A; - address public depositor = makeAddr("Depositor"); + address public depositor = 0x22162DbBa43fE0477cdC5234E248264eC7C6EA7c; IERC20 ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); /// @dev A function invoked before each test case is run. @@ -64,26 +64,23 @@ contract RenzoStrategyTest is PRBTest, StdCheats, StdUtils, DeploymentUtils { vm.stopPrank(); } - function test_DepositToRenzo(uint256 amount) external { - // amount = bound(amount, 1 ether, 100 ether); - // vm.prank(owner); - // renzoStrategy.setAutoStake(true); - // vm.startPrank(depositor); - // vm.deal(depositor, amount); - - // // Deposit Eth - // edgelessDeposit.depositEth{ value: amount }(depositor); - // assertEq( - // address(depositor).balance, - // 0, - // "Deposit should have 0 Eth since all Eth was sent to the edgeless edgelessDeposit contract" - // ); - // assertEq(wrappedEth.balanceOf(depositor), amount, "Depositor should have `amount` of wrapped Eth"); - // assertEq(address(renzoStrategy).balance, 0, "EthStrategy should have 0 Eth"); - // assertTrue( - // isWithinPercentage(ezETH.balanceOf(address(renzoStrategy)), amount, 5), "EthStrategy should have 0 Eth" - // ); - // vm.stopPrank(); + function test_DepositToRenzo() external { + vm.prank(owner); + uint256 amountOut = ethStakingStrategy.swapStethToEzEth(); + console2.log("Ez Eth initial strategy balance: ", ezETH.balanceOf(ethStakingStrategy)); + console2.log("Ez Eth initial depositor balance: ", ezETH.balanceOf(ethStakingStrategy)); + // Make sure users can deposit and withdraw funds + vm.startPrank(depositor); + ezETH.approve(address(edgelessDeposit), amountOut); + edgelessDeposit.depositEzETH(1e18); + console2.log("Ez Eth post deposit strategy balance: ", ezETH.balanceOf(ethStakingStrategy)); + console2.log("Ez Eth post deposit strategy balance: ", ezETH.balanceOf(ethStakingStrategy)); + + console2.log("Ez Eth balance: ", ezETH.balanceOf(address(edgelessDeposit))); + + edgelessDeposit.withdrawEzEth(depositor, 1e18); + + } function isWithinPercentage(uint256 value1, uint256 value2, uint8 percentage) internal pure returns (bool) { From 782a7f992b63d96dbe382aa60696b7a2282d4d56 Mon Sep 17 00:00:00 2001 From: Albert Su Date: Tue, 23 Apr 2024 23:58:14 -0700 Subject: [PATCH 10/10] =?UTF-8?q?=E2=9C=85Temp=20stash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/EdgelessDeposit.sol | 26 +-- src/strategies/RenzoStrategy.sol | 42 +---- src/strategies/RenzoStrategyWithUpgrade.sol | 151 ++++++++++++++++++ test/Strategies/RenzoStrategy.t.sol | 55 +++---- .../Strategies/RenzoStrategyWithUpgrade.t.sol | 95 +++++++++++ 5 files changed, 289 insertions(+), 80 deletions(-) create mode 100644 src/strategies/RenzoStrategyWithUpgrade.sol create mode 100644 test/Strategies/RenzoStrategyWithUpgrade.t.sol diff --git a/src/EdgelessDeposit.sol b/src/EdgelessDeposit.sol index 06e0c9d..4cf635e 100644 --- a/src/EdgelessDeposit.sol +++ b/src/EdgelessDeposit.sol @@ -67,19 +67,19 @@ contract EdgelessDeposit is Ownable2StepUpgradeable, UUPSUpgradeable { emit DepositEth(to, msg.sender, amount, amount); } - // /** - // * @notice Withdraw Eth from the Eth pool - // * @param to Address to withdraw Eth to - // * @param amount Amount to withdraw - // */ - // function withdrawEth(address to, uint256 amount) external { - // wrappedEth.burn(msg.sender, amount); - // uint256 withdrawnAmount = stakingManager.withdraw(amount); - // require(withdrawnAmount == amount, "EdgelessDeposit: Withdrawal amount does not match"); - // (bool success, bytes memory data) = to.call{ value: amount }(""); - // if (!success) revert TransferFailed(data); - // emit WithdrawEth(msg.sender, to, amount, amount); - // } + /** + * @notice Withdraw Eth from the Eth pool + * @param to Address to withdraw Eth to + * @param amount Amount to withdraw + */ + function withdrawEth(address to, uint256 amount) external { + wrappedEth.burn(msg.sender, amount); + uint256 withdrawnAmount = stakingManager.withdraw(amount); + require(withdrawnAmount == amount, "EdgelessDeposit: Withdrawal amount does not match"); + (bool success, bytes memory data) = to.call{ value: amount }(""); + if (!success) revert TransferFailed(data); + emit WithdrawEth(msg.sender, to, amount, amount); + } /** * @notice Withdraw Eth from the Eth pool diff --git a/src/strategies/RenzoStrategy.sol b/src/strategies/RenzoStrategy.sol index 1306f6c..d80c954 100644 --- a/src/strategies/RenzoStrategy.sol +++ b/src/strategies/RenzoStrategy.sol @@ -17,10 +17,7 @@ contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgrade uint256 public ethUnderWithdrawal; IRenzo public renzo; IERC20 public ezETH; - IWETH public WETH; - uint24 public STETH_WETH_POOL_FEE; - ISwapRouter public swapRouter; - uint256[45] private __gap; + uint256[48] private __gap; event EthStaked(uint256 amount, uint256 sharesGenerated); event EzEthWithdrawn(uint256 amount); @@ -31,6 +28,7 @@ contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgrade error TransferFailed(bytes data); error OnlyStakingManager(address sender); error RequestIdsMustBeSorted(); + error WithdrawalsNotImplemented(); modifier onlyStakingManager() { if (msg.sender != stakingManager) revert OnlyStakingManager(msg.sender); @@ -47,6 +45,8 @@ contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgrade __Ownable2Step_init(); __UUPSUpgradeable_init(); _transferOwnership(_owner); + renzo = IRenzo(0x74a09653A083691711cF8215a6ab074BB4e99ef5); + ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); } /// -------------------------------- 📝 External Functions 📝 -------------------------------- @@ -77,9 +77,7 @@ contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgrade } function _withdraw(uint256 withdrawnAmount) internal returns (uint256) { - ezETH.transfer(address(stakingManager), withdrawnAmount); - emit EzEthWithdrawn(withdrawnAmount); - return withdrawnAmount; + revert WithdrawalsNotImplemented(); } /// ---------------------------------- 🔓 Admin Functions 🔓 ---------------------------------- @@ -105,36 +103,6 @@ contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgrade emit SetAutoStake(_autoStake); } - function swapStethToEzEth() external onlyOwner returns (uint256 amountOut) { - // Approve the router to spend DAI. - uint256 amountIn = LIDO.balanceOf(address(this)); - TransferHelper.safeApprove(address(LIDO), address(swapRouter), amountIn); - - // We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount. - ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: abi.encodePacked(address(LIDO), STETH_WETH_POOL_FEE, address(WETH)), - recipient: address(this), - deadline: block.timestamp, - amountIn: amountIn, - amountOutMinimum: amountIn * 90 / 100 - }); - - // The call to `exactInputSingle` executes the swap. - amountOut = swapRouter.exactInput(params); - WETH.withdraw(WETH.balanceOf(address(this))); - _deposit(address(this).balance); - // Convert WETH to EzETH - return amountOut; - } - - function setConstants() external onlyOwner { - renzo = IRenzo(0x74a09653A083691711cF8215a6ab074BB4e99ef5); - ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); - swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - STETH_WETH_POOL_FEE = 10_000; - } - /// --------------------------------- 🔎 View Functions 🔍 --------------------------------- function underlyingAssetAmount() external view override returns (uint256) { return address(this).balance + ezETH.balanceOf(address(this)); diff --git a/src/strategies/RenzoStrategyWithUpgrade.sol b/src/strategies/RenzoStrategyWithUpgrade.sol new file mode 100644 index 0000000..390d9a0 --- /dev/null +++ b/src/strategies/RenzoStrategyWithUpgrade.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23; + +import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { IStakingStrategy } from "../interfaces/IStakingStrategy.sol"; +import { IRenzo } from "../interfaces/IRenzo.sol"; +import { IWETH } from "../interfaces/IWETH.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { LIDO } from "../Constants.sol"; +import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import { TransferHelper } from "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; + +contract RenzoStrategy is IStakingStrategy, Ownable2StepUpgradeable, UUPSUpgradeable { + address public stakingManager; + bool public autoStake; + uint256 public ethUnderWithdrawal; + IRenzo public renzo; + IERC20 public ezETH; + IWETH public WETH; + uint24 public STETH_WETH_POOL_FEE; + ISwapRouter public swapRouter; + uint256[45] private __gap; + + event EthStaked(uint256 amount, uint256 sharesGenerated); + event EzEthWithdrawn(uint256 amount); + event SetStakingManager(address stakingManager); + event SetAutoStake(bool autoStake); + + error InsufficientFunds(); + error TransferFailed(bytes data); + error OnlyStakingManager(address sender); + error RequestIdsMustBeSorted(); + + modifier onlyStakingManager() { + if (msg.sender != stakingManager) revert OnlyStakingManager(msg.sender); + _; + } + + constructor() { + _disableInitializers(); + } + + function initialize(address _owner, address _stakingManager) external initializer { + stakingManager = _stakingManager; + autoStake = true; + __Ownable2Step_init(); + __UUPSUpgradeable_init(); + _transferOwnership(_owner); + renzo = IRenzo(0x74a09653A083691711cF8215a6ab074BB4e99ef5); + ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); + swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + STETH_WETH_POOL_FEE = 10_000; + } + + /// -------------------------------- 📝 External Functions 📝 -------------------------------- + function deposit(uint256 amount) external payable override onlyStakingManager { + if (!autoStake) return; + _deposit(amount); + } + + function depositEzETH(uint256 amount) external payable onlyStakingManager { + ezETH.transferFrom(msg.sender, address(this), amount); + } + + function withdraw(uint256 amount) external override onlyStakingManager returns (uint256 withdrawnAmount) { + uint256 balance = address(this).balance; + if (amount > balance) { + withdrawnAmount = balance; + } else { + withdrawnAmount = amount; + } + return _withdraw(withdrawnAmount); + } + + /// --------------------------------- 🛠️ Internal Functions 🛠️ --------------------------------- + function _deposit(uint256 amount) internal { + if (amount > address(this).balance) revert InsufficientFunds(); + renzo.depositETH{ value: amount }(); + emit EthStaked(amount, amount); + } + + function _withdraw(uint256 withdrawnAmount) internal returns (uint256) { + ezETH.transfer(address(stakingManager), withdrawnAmount); + emit EzEthWithdrawn(withdrawnAmount); + return withdrawnAmount; + } + + /// ---------------------------------- 🔓 Admin Functions 🔓 ---------------------------------- + function ownerDeposit(uint256 amount) external payable override onlyOwner { + _deposit(amount); + } + + function ownerDepositEzEth(uint256 amount) external payable onlyOwner { + ezETH.transferFrom(msg.sender, address(this), amount); + } + + function ownerWithdraw(uint256 amount) external override onlyOwner returns (uint256 withdrawnAmount) { + return _withdraw(amount); + } + + function setStakingManager(address _stakingManager) external override onlyOwner { + stakingManager = _stakingManager; + emit SetStakingManager(_stakingManager); + } + + function setAutoStake(bool _autoStake) external override onlyOwner { + autoStake = _autoStake; + emit SetAutoStake(_autoStake); + } + + function swapStethToEzEth() external onlyOwner returns (uint256 amountOut) { + // Approve the router to spend DAI. + uint256 amountIn = LIDO.balanceOf(address(this)); + TransferHelper.safeApprove(address(LIDO), address(swapRouter), amountIn); + + // We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount. + ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ + path: abi.encodePacked(address(LIDO), STETH_WETH_POOL_FEE, address(WETH)), + recipient: address(this), + deadline: block.timestamp, + amountIn: amountIn, + amountOutMinimum: amountIn * 90 / 100 + }); + + // The call to `exactInputSingle` executes the swap. + amountOut = swapRouter.exactInput(params); + WETH.withdraw(WETH.balanceOf(address(this))); + _deposit(address(this).balance); + // Convert WETH to EzETH + return amountOut; + } + + function setConstants() external onlyOwner { + renzo = IRenzo(0x74a09653A083691711cF8215a6ab074BB4e99ef5); + ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); + swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + STETH_WETH_POOL_FEE = 10_000; + } + + /// --------------------------------- 🔎 View Functions 🔍 --------------------------------- + function underlyingAssetAmount() external view override returns (uint256) { + return address(this).balance + ezETH.balanceOf(address(this)); + } + + receive() external payable { } + + function _authorizeUpgrade(address) internal override onlyOwner { } +} diff --git a/test/Strategies/RenzoStrategy.t.sol b/test/Strategies/RenzoStrategy.t.sol index fad989e..0c3f2e6 100644 --- a/test/Strategies/RenzoStrategy.t.sol +++ b/test/Strategies/RenzoStrategy.t.sol @@ -29,7 +29,7 @@ contract RenzoStrategyTest is PRBTest, StdCheats, StdUtils, DeploymentUtils { EdgelessDeposit internal edgelessDeposit = EdgelessDeposit(payable(0x7E0bc314535f430122caFEF18eAbd508d62934bf)); WrappedToken internal wrappedEth = WrappedToken(0xcD0aa40948c662dEDd9F157085fd6369A255F2f7); StakingManager internal stakingManager = StakingManager(payable(0x1e6d08769be5Dc83d38C64C5776305Ad6F01c227)); - RenzoStrategy internal ethStakingStrategy = RenzoStrategy(payable(0xbD95aa0f68B95e6C01d02F1a36D8fde29C6C8e7b)); + RenzoStrategy internal ethStakingStrategy; IStakingStrategy internal renzoStrategy; uint32 public constant FORK_BLOCK_NUMBER = 19_722_752; @@ -48,39 +48,34 @@ contract RenzoStrategyTest is PRBTest, StdCheats, StdUtils, DeploymentUtils { // Upgrade contracts vm.startPrank(owner); - address edgelessDepositImpl = address(new EdgelessDeposit()); - bytes memory edgelessDepositData = abi.encodeCall(EdgelessDeposit.upgrade, ()); - edgelessDeposit.upgradeToAndCall(edgelessDepositImpl, edgelessDepositData); - - address stakingManagerImpl = address(new StakingManager()); - bytes memory stakingManagerData = - abi.encodeCall(StakingManager.setEzETH, (0xbf5495Efe5DB9ce00f80364C8B423567e58d2110)); - stakingManager.upgradeToAndCall(stakingManagerImpl, stakingManagerData); - - address renzoStrategyImpl = address(new RenzoStrategy()); - bytes memory renzoStrategyData = abi.encodeCall(RenzoStrategy.setConstants, ()); - ethStakingStrategy.upgradeToAndCall(renzoStrategyImpl, renzoStrategyData); - + (stakingManager, edgelessDeposit, wrappedEth, ) = deployContracts(owner); + address EthStakingStrategyImpl = address(new RenzoStrategy()); + bytes memory EthStakingStrategyData = abi.encodeCall(RenzoStrategy.initialize, (owner, address(stakingManager))); + ethStakingStrategy = + IStakingStrategy(payable(address(new ERC1967Proxy(EthStakingStrategyImpl, EthStakingStrategyData)))); + stakingManager.addStrategy(stakingManager.ETH_ADDRESS(), address(ethStakingStrategy)); + stakingManager.setActiveStrategy(stakingManager.ETH_ADDRESS(), 1); vm.stopPrank(); } - function test_DepositToRenzo() external { - vm.prank(owner); - uint256 amountOut = ethStakingStrategy.swapStethToEzEth(); - console2.log("Ez Eth initial strategy balance: ", ezETH.balanceOf(ethStakingStrategy)); - console2.log("Ez Eth initial depositor balance: ", ezETH.balanceOf(ethStakingStrategy)); - // Make sure users can deposit and withdraw funds + function test_EzEthDepositAndWithdraw(uint256 amount) external { + amount = bound(amount, 1e18, 1e40); vm.startPrank(depositor); - ezETH.approve(address(edgelessDeposit), amountOut); - edgelessDeposit.depositEzETH(1e18); - console2.log("Ez Eth post deposit strategy balance: ", ezETH.balanceOf(ethStakingStrategy)); - console2.log("Ez Eth post deposit strategy balance: ", ezETH.balanceOf(ethStakingStrategy)); - - console2.log("Ez Eth balance: ", ezETH.balanceOf(address(edgelessDeposit))); - - edgelessDeposit.withdrawEzEth(depositor, 1e18); - - + vm.deal(depositor, amount); + vm.deal(address(edgelessDeposit), amount); + + // Deposit Eth + edgelessDeposit.depositEth{ value: amount }(depositor); + assertEq( + address(depositor).balance, + 0, + "Deposit should have 0 Eth since all Eth was sent to the edgeless edgelessDeposit contract" + ); + assertEq(wrappedEth.balanceOf(depositor), amount, "Depositor should have `amount` of wrapped Eth"); + + // edgelessDeposit.withdrawEth(depositor, amount); + assertGt(ezETH.balanceOf(ethStakingStrategy), amount, "Depositor should have `amount` of Eth after withdrawing"); + // assertEq(wrappedEth.balanceOf(depositor), 0, "Depositor should have 0 wrapped Eth after withdrawing"); } function isWithinPercentage(uint256 value1, uint256 value2, uint8 percentage) internal pure returns (bool) { diff --git a/test/Strategies/RenzoStrategyWithUpgrade.t.sol b/test/Strategies/RenzoStrategyWithUpgrade.t.sol new file mode 100644 index 0000000..fad989e --- /dev/null +++ b/test/Strategies/RenzoStrategyWithUpgrade.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.23 <0.9.0; + +import "forge-std/src/Vm.sol"; +import { PRBTest } from "@prb/test/src/PRBTest.sol"; +import { console2 } from "forge-std/src/console2.sol"; +import { StdCheats } from "forge-std/src/StdCheats.sol"; +import { StdUtils } from "forge-std/src/StdUtils.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import { EdgelessDeposit } from "../../src/EdgelessDeposit.sol"; +import { StakingManager } from "../../src/StakingManager.sol"; +import { WrappedToken } from "../../src/WrappedToken.sol"; +import { RenzoStrategy } from "../../src/strategies/RenzoStrategy.sol"; + +import { IWithdrawalQueueERC721 } from "../../src/interfaces/IWithdrawalQueueERC721.sol"; +import { IStakingStrategy } from "../../src/interfaces/IStakingStrategy.sol"; + +import { Permit, SigUtils } from "../Utils/SigUtils.sol"; +import { DeploymentUtils } from "../Utils/DeploymentUtils.sol"; +import { UpgradedEdgelessDeposit } from "../../src/upgrade-tests/UpgradedEdgelessDeposit.sol"; + +/// @dev If this is your first time with Forge, read this tutorial in the Foundry Book: +/// https://book.getfoundry.sh/forge/writing-tests +contract RenzoStrategyTest is PRBTest, StdCheats, StdUtils, DeploymentUtils { + using SigUtils for Permit; + + EdgelessDeposit internal edgelessDeposit = EdgelessDeposit(payable(0x7E0bc314535f430122caFEF18eAbd508d62934bf)); + WrappedToken internal wrappedEth = WrappedToken(0xcD0aa40948c662dEDd9F157085fd6369A255F2f7); + StakingManager internal stakingManager = StakingManager(payable(0x1e6d08769be5Dc83d38C64C5776305Ad6F01c227)); + RenzoStrategy internal ethStakingStrategy = RenzoStrategy(payable(0xbD95aa0f68B95e6C01d02F1a36D8fde29C6C8e7b)); + IStakingStrategy internal renzoStrategy; + + uint32 public constant FORK_BLOCK_NUMBER = 19_722_752; + + address public owner = 0xcB58d1142e53e37aDE44E1F125248FbfAc99352A; + address public depositor = 0x22162DbBa43fE0477cdC5234E248264eC7C6EA7c; + IERC20 ezETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); + + /// @dev A function invoked before each test case is run. + function setUp() public virtual { + string memory alchemyApiKey = vm.envOr("API_KEY_ALCHEMY", string("")); + vm.createSelectFork({ + urlOrAlias: string(abi.encodePacked("https://Eth-mainnet.g.alchemy.com/v2/", alchemyApiKey)), + blockNumber: FORK_BLOCK_NUMBER + }); + + // Upgrade contracts + vm.startPrank(owner); + address edgelessDepositImpl = address(new EdgelessDeposit()); + bytes memory edgelessDepositData = abi.encodeCall(EdgelessDeposit.upgrade, ()); + edgelessDeposit.upgradeToAndCall(edgelessDepositImpl, edgelessDepositData); + + address stakingManagerImpl = address(new StakingManager()); + bytes memory stakingManagerData = + abi.encodeCall(StakingManager.setEzETH, (0xbf5495Efe5DB9ce00f80364C8B423567e58d2110)); + stakingManager.upgradeToAndCall(stakingManagerImpl, stakingManagerData); + + address renzoStrategyImpl = address(new RenzoStrategy()); + bytes memory renzoStrategyData = abi.encodeCall(RenzoStrategy.setConstants, ()); + ethStakingStrategy.upgradeToAndCall(renzoStrategyImpl, renzoStrategyData); + + vm.stopPrank(); + } + + function test_DepositToRenzo() external { + vm.prank(owner); + uint256 amountOut = ethStakingStrategy.swapStethToEzEth(); + console2.log("Ez Eth initial strategy balance: ", ezETH.balanceOf(ethStakingStrategy)); + console2.log("Ez Eth initial depositor balance: ", ezETH.balanceOf(ethStakingStrategy)); + // Make sure users can deposit and withdraw funds + vm.startPrank(depositor); + ezETH.approve(address(edgelessDeposit), amountOut); + edgelessDeposit.depositEzETH(1e18); + console2.log("Ez Eth post deposit strategy balance: ", ezETH.balanceOf(ethStakingStrategy)); + console2.log("Ez Eth post deposit strategy balance: ", ezETH.balanceOf(ethStakingStrategy)); + + console2.log("Ez Eth balance: ", ezETH.balanceOf(address(edgelessDeposit))); + + edgelessDeposit.withdrawEzEth(depositor, 1e18); + + + } + + function isWithinPercentage(uint256 value1, uint256 value2, uint8 percentage) internal pure returns (bool) { + require(percentage > 0 && percentage <= 100, "Percentage must be between 1 and 100"); + + // Calculate the margin of error + uint256 margin = (value1 * percentage) / 100; + + // Check if value2 is within the acceptable range + return value2 >= value1 - margin && value2 <= value1 + margin; + } +}