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; 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/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/EdgelessDeposit.sol b/src/EdgelessDeposit.sol index c1f21de..4cf635e 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 EzEthAmountWithdrew, uint256 burnAmount); error MaxMintExceeded(); error TransferFailed(bytes data); @@ -58,6 +60,13 @@ contract EdgelessDeposit is Ownable2StepUpgradeable, UUPSUpgradeable { emit DepositEth(to, msg.sender, msg.value, 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 @@ -72,6 +81,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, withdrawnAmount); + emit WithdrawEzEth(msg.sender, to, withdrawnAmount, amount); + } + /// ---------------------------------- 🔓 Admin Functions 🔓 ---------------------------------- /** * @notice Mint wrapped tokens based on the amount of Eth staked @@ -86,7 +107,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 7a96172..69d7024 100644 --- a/src/StakingManager.sol +++ b/src/StakingManager.sol @@ -18,10 +18,13 @@ 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 StakeEzEth(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); @@ -57,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); } @@ -73,12 +86,27 @@ 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; 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/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/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 new file mode 100644 index 0000000..d80c954 --- /dev/null +++ b/src/strategies/RenzoStrategy.sol @@ -0,0 +1,114 @@ +// 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; + 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(); + error WithdrawalsNotImplemented(); + + 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 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) { + revert WithdrawalsNotImplemented(); + } + + /// ---------------------------------- 🔓 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); + } + + /// --------------------------------- 🔎 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/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 new file mode 100644 index 0000000..0c3f2e6 --- /dev/null +++ b/test/Strategies/RenzoStrategy.t.sol @@ -0,0 +1,90 @@ +// 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; + 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); + (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_EzEthDepositAndWithdraw(uint256 amount) external { + amount = bound(amount, 1e18, 1e40); + vm.startPrank(depositor); + 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) { + 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; + } +} 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; + } +}