diff --git a/audits/2025.10.06 - Certora - EtherFi - stETH Withdrawals.pdf b/audits/2025.10.06 - Certora - EtherFi - stETH Withdrawals.pdf new file mode 100644 index 00000000..99f91945 Binary files /dev/null and b/audits/2025.10.06 - Certora - EtherFi - stETH Withdrawals.pdf differ diff --git a/deployment/EtherFiRedemptionManager/2025-10-09-18-23-23.json b/deployment/EtherFiRedemptionManager/2025-10-09-18-23-23.json new file mode 100644 index 00000000..f4f3da65 --- /dev/null +++ b/deployment/EtherFiRedemptionManager/2025-10-09-18-23-23.json @@ -0,0 +1,16 @@ +{ + "contractName": "EtherFiRedemptionManager", + "deploymentParameters": { + "factory": "0x356d1B83970CeF2018F2c9337cDdb67dff5AEF99", + "salt": "0x037da63f453b943e7bd96c155e0798003094e4a0000000000000000000000000", + "constructorArgs": { + "_liquidityPool": "0x308861A430be4cce5502d0A12724771Fc6DaF216", + "_eEth": "0x35fA164735182de50811E8e2E824cFb9B6118ac2", + "_weEth": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", + "_treasury": "0x0c83EAe1FE72c390A02E426572854931EefF93BA", + "_roleRegistry": "0x62247D29B4B9BECf4BB73E0c722cf6445cfC7cE9", + "_etherFiRestaker": "0x1B7a4C3797236A1C37f8741c0Be35c2c72736fFf" + } + }, + "deployedAddress": "0xE3F384Dc7002547Dd240AC1Ad69a430CCE1e292d" +} \ No newline at end of file diff --git a/deployment/EtherFiRedemptionManagerTemp/2025-10-09-18-23-23.json b/deployment/EtherFiRedemptionManagerTemp/2025-10-09-18-23-23.json new file mode 100644 index 00000000..6b592877 --- /dev/null +++ b/deployment/EtherFiRedemptionManagerTemp/2025-10-09-18-23-23.json @@ -0,0 +1,15 @@ +{ + "contractName": "EtherFiRedemptionManagerTemp", + "deploymentParameters": { + "factory": "0x356d1B83970CeF2018F2c9337cDdb67dff5AEF99", + "salt": "0x037da63f453b943e7bd96c155e0798003094e4a0000000000000000000000000", + "constructorArgs": { + "_liquidityPool": "0x308861A430be4cce5502d0A12724771Fc6DaF216", + "_eEth": "0x35fA164735182de50811E8e2E824cFb9B6118ac2", + "_weEth": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", + "_treasury": "0x0c83EAe1FE72c390A02E426572854931EefF93BA", + "_roleRegistry": "0x62247D29B4B9BECf4BB73E0c722cf6445cfC7cE9" + } + }, + "deployedAddress": "0x590015FDf9334594B0Ae14f29b0dEd9f1f8504Bc" +} \ No newline at end of file diff --git a/deployment/EtherFiRestaker/2025-10-09-18-23-23.json b/deployment/EtherFiRestaker/2025-10-09-18-23-23.json new file mode 100644 index 00000000..a7e5525d --- /dev/null +++ b/deployment/EtherFiRestaker/2025-10-09-18-23-23.json @@ -0,0 +1,12 @@ +{ + "contractName": "EtherFiRestaker", + "deploymentParameters": { + "factory": "0x356d1B83970CeF2018F2c9337cDdb67dff5AEF99", + "salt": "0x037da63f453b943e7bd96c155e0798003094e4a0000000000000000000000000", + "constructorArgs": { + "_rewardsCoordinator": "0x7750d328b314EfFa365A0402CcfD489B80B0adda", + "_etherFiRedemptionManager": "0xDadEf1fFBFeaAB4f68A9fD181395F68b4e4E7Ae0" + } + }, + "deployedAddress": "0x71bEf55739F0b148E2C3e645FDE947f380C48615" +} \ No newline at end of file diff --git a/deployment/LiquidityPool/2025-08-20-00-49-35.json b/deployment/LiquidityPool/2025-10-09-18-23-23.json similarity index 58% rename from deployment/LiquidityPool/2025-08-20-00-49-35.json rename to deployment/LiquidityPool/2025-10-09-18-23-23.json index 9358a99d..e508b12a 100644 --- a/deployment/LiquidityPool/2025-08-20-00-49-35.json +++ b/deployment/LiquidityPool/2025-10-09-18-23-23.json @@ -2,9 +2,9 @@ "contractName": "LiquidityPool", "deploymentParameters": { "factory": "0x356d1B83970CeF2018F2c9337cDdb67dff5AEF99", - "salt": "0x7972bd777a339ca98eff1677484aacc816b24d87000000000000000000000000", + "salt": "0x037da63f453b943e7bd96c155e0798003094e4a0000000000000000000000000", "constructorArgs": { } }, - "deployedAddress": "0x025911766aEF6fF0C294FD831a2b5c17dC299B3f" + "deployedAddress": "0xA5C1ddD9185901E3c05E0660126627E039D0a626" } \ No newline at end of file diff --git a/script/ContractCodeChecker.sol b/script/ContractCodeChecker.sol index 9ca03806..8493052b 100644 --- a/script/ContractCodeChecker.sol +++ b/script/ContractCodeChecker.sol @@ -97,7 +97,7 @@ contract ContractCodeChecker { bytes memory localBytecode = address(localDeployed).code; bytes memory onchainRuntimeBytecode = address(deployedImpl).code; - if (compareBytes(localBytecode, onchainRuntimeBytecode)) { + if (compareBytes(onchainRuntimeBytecode, localBytecode)) { console2.log("-> Full Bytecode Match: Success\n"); } else { console2.log("-> Full Bytecode Match: Fail\n"); @@ -126,7 +126,7 @@ contract ContractCodeChecker { } // Compare trimmed arrays byte-by-byte - if (compareBytes(trimmedLocal, trimmedOnchain)) { + if (compareBytes(trimmedOnchain, trimmedLocal)) { console2.log("-> Partial Bytecode Match: Success\n"); } else { console2.log("-> Partial Bytecode Match: Fail\n"); diff --git a/script/stETH-withdrawals/deploy.s.sol b/script/stETH-withdrawals/deploy.s.sol new file mode 100644 index 00000000..0cdcaf22 --- /dev/null +++ b/script/stETH-withdrawals/deploy.s.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "forge-std/Script.sol"; +import {Utils} from "../utils/utils.sol"; +import {EtherFiRedemptionManagerTemp} from "../../src/EtherFiRedemptionManagerTemp.sol"; +import {EtherFiRestaker} from "../../src/EtherFiRestaker.sol"; +import {EtherFiRedemptionManager} from "../../src/EtherFiRedemptionManager.sol"; +import {LiquidityPool} from "../../src/LiquidityPool.sol"; +import {ICreate2Factory} from "../utils/utils.sol"; + +contract DeployInstanstStETHWithdrawals is Script, Utils { + ICreate2Factory constant factory = ICreate2Factory(0x356d1B83970CeF2018F2c9337cDdb67dff5AEF99); + //-------------------------------------------------------------------------------------- + //---------------------------- New Deployments ----------------------------------------- + //-------------------------------------------------------------------------------------- + address liquidityPoolImpl; + address etherFiRedemptionManagerTempImpl; + address etherFiRestakerImpl; + address etherFiRedemptionManagerImpl; + + //-------------------------------------------------------------------------------------- + //------------------------- Existing Users/Proxies ------------------------------------- + //-------------------------------------------------------------------------------------- + address constant rewardsCoordinator = 0x7750d328b314EfFa365A0402CcfD489B80B0adda; // Eigen Layer Rewards Coordinator - https://etherscan.io/address/0x7750d328b314effa365a0402ccfd489b80b0adda + address constant etherFiRedemptionManager = 0xDadEf1fFBFeaAB4f68A9fD181395F68b4e4E7Ae0; + address constant eETH = 0x35fA164735182de50811E8e2E824cFb9B6118ac2; + address constant weETH = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee; + address constant liquidityPool = 0x308861A430be4cce5502d0A12724771Fc6DaF216; + address constant roleRegistry = 0x62247D29B4B9BECf4BB73E0c722cf6445cfC7cE9; + address constant treasury = 0x0c83EAe1FE72c390A02E426572854931EefF93BA; + address constant etherFiRestaker = 0x1B7a4C3797236A1C37f8741c0Be35c2c72736fFf; + + // TODO: update with final commit + bytes32 commitHashSalt = bytes32(bytes20(hex"037da63f453b943e7bd96c155e0798003094e4a0")); + + function run() external { + vm.startBroadcast(); + + { + string memory contractName = "EtherFiRedemptionManagerTemp"; + bytes memory constructorArgs = abi.encode( + address(liquidityPool), + address(eETH), + address(weETH), + address(treasury), + address(roleRegistry) + ); + bytes memory bytecode = abi.encodePacked( + type(EtherFiRedemptionManagerTemp).creationCode, + constructorArgs + ); + etherFiRedemptionManagerTempImpl = deploy(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); + verify(etherFiRedemptionManagerTempImpl, bytecode, commitHashSalt, factory); + } + + { + string memory contractName = "EtherFiRestaker"; + bytes memory constructorArgs = abi.encode( + address(rewardsCoordinator), + address(etherFiRedemptionManager) + ); + bytes memory bytecode = abi.encodePacked( + type(EtherFiRestaker).creationCode, + constructorArgs + ); + etherFiRestakerImpl = deploy(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); + verify(etherFiRestakerImpl, bytecode, commitHashSalt, factory); + } + + { + string memory contractName = "EtherFiRedemptionManager"; + bytes memory constructorArgs = abi.encode( + address(liquidityPool), + address(eETH), + address(weETH), + address(treasury), + address(roleRegistry), + address(etherFiRestaker) + ); + bytes memory bytecode = abi.encodePacked( + type(EtherFiRedemptionManager).creationCode, + constructorArgs + ); + etherFiRedemptionManagerImpl = deploy(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); + verify(etherFiRedemptionManagerImpl, bytecode, commitHashSalt, factory); + } + + { + string memory contractName = "LiquidityPool"; + bytes memory constructorArgs = abi.encode( + ); + bytes memory bytecode = abi.encodePacked( + type(LiquidityPool).creationCode, + constructorArgs + ); + liquidityPoolImpl = deploy(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); + verify(liquidityPoolImpl, bytecode, commitHashSalt, factory); + } + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/script/stETH-withdrawals/transactions.s.sol b/script/stETH-withdrawals/transactions.s.sol new file mode 100644 index 00000000..5ccf1a9e --- /dev/null +++ b/script/stETH-withdrawals/transactions.s.sol @@ -0,0 +1,487 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "forge-std/Script.sol"; +import "forge-std/console2.sol"; +import {Utils} from "../utils/utils.sol"; +import {EtherFiTimelock} from "../../src/EtherFiTimelock.sol"; +import {UUPSUpgradeable} from "lib/openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {EtherFiRedemptionManagerTemp} from "../../src/EtherFiRedemptionManagerTemp.sol"; +import {EtherFiRestaker} from "../../src/EtherFiRestaker.sol"; +import {EtherFiRedemptionManager} from "../../src/EtherFiRedemptionManager.sol"; +import {LiquidityPool} from "../../src/LiquidityPool.sol"; + +contract StETHWithdrawalsTransactions is Script, Utils { + EtherFiTimelock etherFiTimelock = EtherFiTimelock(payable(0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761)); + EtherFiTimelock etherfiOperatingTimelock = EtherFiTimelock(payable(0xcD425f44758a08BaAB3C4908f3e3dE5776e45d7a)); + EtherFiRestaker etherFiRestakerInstance = EtherFiRestaker(payable(0x1B7a4C3797236A1C37f8741c0Be35c2c72736fFf)); + //-------------------------------------------------------------------------------------- + //--------------------- Previous Implementations --------------------------------------- + //-------------------------------------------------------------------------------------- + address constant oldLiquidityPoolImpl = 0x025911766aEF6fF0C294FD831a2b5c17dC299B3f; + address constant oldEtherFiRedemptionManagerImpl = 0xe6f40295A7500509faD08E924c91b0F050a7b84b; + address constant oldEtherFiRestakerImpl = 0x0052F731a6BEA541843385ffBA408F52B74Cb624; + + // https://etherscan.io/address/0xDadEf1fFBFeaAB4f68A9fD181395F68b4e4E7Ae0#readProxyContract + uint16 constant oldExitFeeSplitToTreasuryInBps = 1000; + uint16 constant oldExitFeeInBps = 30; + uint16 constant oldLowWatermarkInBpsOfTvl = 100; + uint64 constant oldRefillRatePerSecond = 23148; + uint64 constant oldCapacity = 2000000000; + + //-------------------------------------------------------------------------------------- + //---------------------------- New Deployments ----------------------------------------- + //-------------------------------------------------------------------------------------- + LiquidityPool liquidityPoolImpl = LiquidityPool(payable(0xA5C1ddD9185901E3c05E0660126627E039D0a626)); + EtherFiRedemptionManagerTemp etherFiRedemptionManagerTempImpl = EtherFiRedemptionManagerTemp(payable(0x590015FDf9334594B0Ae14f29b0dEd9f1f8504Bc)); + EtherFiRestaker etherFiRestakerImpl = EtherFiRestaker(payable(0x71bEf55739F0b148E2C3e645FDE947f380C48615)); + EtherFiRedemptionManager etherFiRedemptionManagerImpl = EtherFiRedemptionManager(payable(0xE3F384Dc7002547Dd240AC1Ad69a430CCE1e292d)); + + //-------------------------------------------------------------------------------------- + //------------------------- Existing Users/Proxies ------------------------------------- + //-------------------------------------------------------------------------------------- + address constant rewardsCoordinator = 0x7750d328b314EfFa365A0402CcfD489B80B0adda; // Eigen Layer Rewards Coordinator - https://etherscan.io/address/0x7750d328b314effa365a0402ccfd489b80b0adda + address constant etherFiRedemptionManager = 0xDadEf1fFBFeaAB4f68A9fD181395F68b4e4E7Ae0; + address constant eETH = 0x35fA164735182de50811E8e2E824cFb9B6118ac2; + address constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address constant weETH = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee; + address constant stETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; + address constant liquidityPool = 0x308861A430be4cce5502d0A12724771Fc6DaF216; + address constant operatingTimelock = 0xcD425f44758a08BaAB3C4908f3e3dE5776e45d7a; + address constant roleRegistry = 0x62247D29B4B9BECf4BB73E0c722cf6445cfC7cE9; + address constant treasury = 0x0c83EAe1FE72c390A02E426572854931EefF93BA; + address constant etherFiRestaker = 0x1B7a4C3797236A1C37f8741c0Be35c2c72736fFf; + + address constant ETHERFI_OPERATING_ADMIN = 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; + address constant TIMELOCK_CONTROLLER = 0xcdd57D11476c22d265722F68390b036f3DA48c21; + + //-------------------------------------------------------------------------------------- + //----------------------------- OLD EFRM SELECTORS ----------------------------------- + //-------------------------------------------------------------------------------------- + bytes4 constant SET_EXIT_FEE_BASIS_POINTS_SELECTOR = 0xad0cba24; + bytes4 constant SET_EXIT_FEE_SPLIT_TO_TREASURY_IN_BPS_SELECTOR = 0x69b095a2; + bytes4 constant SET_LOW_WATERMARK_IN_BPS_OF_TVL_SELECTOR = 0x298f3f03; + bytes4 constant SET_REFILL_RATE_PER_SECOND_SELECTOR = 0x2f530824; + bytes4 constant SET_CAPACITY_SELECTOR = 0x91915ef8; + + function run() external { + console2.log("StETH Withdrawals Transactions"); + console2.log("================================================"); + console2.log(""); + + vm.startBroadcast(TIMELOCK_CONTROLLER); + // vm.startPrank(TIMELOCK_CONTROLLER); + scheduleCleanUpStorageOnEFRM(); + upgrade(); + // vm.stopPrank(); + vm.stopBroadcast(); + + vm.startBroadcast(ETHERFI_OPERATING_ADMIN); + // vm.startPrank(ETHERFI_OPERATING_ADMIN); + initializeTokenParametersEFRM(); + // vm.stopPrank(); + vm.stopBroadcast(); + + console2.log("=============== ROLLBACK TRANSACTIONS ================"); + console2.log("================================================"); + + vm.startBroadcast(TIMELOCK_CONTROLLER); + // vm.startPrank(TIMELOCK_CONTROLLER); + rollbackUpgrade(); + // vm.stopPrank(); + vm.stopBroadcast(); + + vm.startBroadcast(ETHERFI_OPERATING_ADMIN); + // vm.startPrank(ETHERFI_OPERATING_ADMIN); + rollbackEFRMStorage(); + // vm.stopPrank(); + vm.stopBroadcast(); + } + + function scheduleCleanUpStorageOnEFRM() public { + bytes32 firstTxId = _upgradeEFRMToTemp(); + _clearOutSlotForUpgrade(firstTxId); + } + + function _upgradeEFRMToTemp() internal returns (bytes32) { + address[] memory targets = new address[](1); + bytes[] memory data = new bytes[](1); + uint256[] memory values = new uint256[](1); + + targets[0] = address(etherFiRedemptionManager); + data[0] = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, etherFiRedemptionManagerTempImpl); + + bytes32 timelockSalt = keccak256(abi.encode(targets, data, block.number)); + bytes32 operationId = etherFiTimelock.hashOperationBatch(targets, values, data, bytes32(0), timelockSalt); + + bytes memory scheduleCalldata = abi.encodeWithSelector( + etherFiTimelock.scheduleBatch.selector, + targets, + values, + data, + bytes32(0)/*=predecessor*/, + timelockSalt, + MIN_DELAY_TIMELOCK/*=minDelay*/ + ); + + console2.log("Schedule Upgrade EFRM To Temp Tx:"); + console2.logBytes(scheduleCalldata); + console2.log("================================================"); + console2.log(""); + + bytes memory executeCalldata = abi.encodeWithSelector( + etherFiTimelock.executeBatch.selector, + targets, + values, + data, + bytes32(0)/*=predecessor*/, + timelockSalt + ); + + console2.log("Execute Upgrade EFRM To Temp Tx:"); + console2.logBytes(executeCalldata); + console2.log("================================================"); + console2.log(""); + + // uncomment to run against fork + console2.log("=== SCHEDULING BATCH ==="); + console2.log("Current timestamp:", block.timestamp); + etherFiTimelock.scheduleBatch(targets, values, data, bytes32(0), timelockSalt, MIN_DELAY_TIMELOCK); + console2.log("Schedule of Upgrade EFRM To Temp successful"); + console2.log("Operation ID:", vm.toString(operationId)); + console2.log("================================================"); + console2.log(""); + + // console2.log("=== FAST FORWARDING TIME ==="); + // vm.warp(block.timestamp + MIN_DELAY_TIMELOCK + 1); // +1 to ensure it's past the delay + // console2.log("New timestamp:", block.timestamp); + // etherFiTimelock.executeBatch(targets, values, data, bytes32(0), timelockSalt); + // console2.log("Execute of Upgrade EFRM To Temp successful"); + // console2.log("================================================"); + // console2.log(""); + + return operationId; + } + + function _clearOutSlotForUpgrade(bytes32 predecessor) internal { + address[] memory targets = new address[](1); + bytes[] memory data = new bytes[](1); + uint256[] memory values = new uint256[](1); + + targets[0] = address(etherFiRedemptionManager); + data[0] = abi.encodeWithSelector(EtherFiRedemptionManagerTemp.clearOutSlotForUpgrade.selector); + + //-------------------------------------------------------------------------------------- + //------------------------------- SCHEDULE TX -------------------------------------- + //------------------------------------------------ -------------------------------------- + bytes32 timelockSalt = keccak256(abi.encode(targets, data, block.number)); + bytes32 operationId = etherFiTimelock.hashOperationBatch(targets, values, data, predecessor, timelockSalt); + + bytes memory scheduleCalldata = abi.encodeWithSelector( + etherFiTimelock.scheduleBatch.selector, + targets, + values, + data, + predecessor, // Use the predecessor from the first transaction + timelockSalt, + MIN_DELAY_TIMELOCK/*=minDelay*/ + ); + + console2.log("Schedule Clean Up Storage On EFRM Tx:"); + console2.logBytes(scheduleCalldata); + console2.log("================================================"); + console2.log(""); + + // execute + bytes memory executeCalldata = abi.encodeWithSelector( + etherFiTimelock.executeBatch.selector, + targets, + values, + data, + predecessor, // Use the predecessor from the first transaction + timelockSalt + ); + + console2.log("Execute Clean Up Storage On EFRM Tx:"); + console2.logBytes(executeCalldata); + console2.log("================================================"); + console2.log(""); + + // uncomment to run against fork + console2.log("=== SCHEDULING BATCH ==="); + console2.log("Current timestamp:", block.timestamp); + console2.log("Predecessor:", vm.toString(predecessor)); + etherFiTimelock.scheduleBatch(targets, values, data, predecessor, timelockSalt, MIN_DELAY_TIMELOCK); + console2.log("Operation ID:", vm.toString(operationId)); + console2.log("================================================"); + console2.log(""); + console2.log("scheduled clearOutSlotForUpgrade Tx:"); + + // console2.log("=== FAST FORWARDING TIME ==="); + // vm.warp(block.timestamp + MIN_DELAY_TIMELOCK + 1); // +1 to ensure it's past the delay + // console2.log("New timestamp:", block.timestamp); + // etherFiTimelock.executeBatch(targets, values, data, predecessor, timelockSalt); + // console2.log("Execute of Clean Up Storage On EFRM Tx successful"); + // console2.log("================================================"); + // console2.log(""); + } + + function initializeTokenParametersEFRM() public { + address[] memory targets = new address[](1); + bytes[] memory data = new bytes[](1); + uint256[] memory values = new uint256[](1); + + address[] memory _tokens = new address[](2); + _tokens[0] = ETH; + _tokens[1] = stETH; + uint16[] memory _exitFeeSplitToTreasuryInBps = new uint16[](2); + _exitFeeSplitToTreasuryInBps[0] = 1000; + _exitFeeSplitToTreasuryInBps[1] = 1000; + uint16[] memory _exitFeeInBps = new uint16[](2); + _exitFeeInBps[0] = 30; + _exitFeeInBps[1] = 0; + uint16[] memory _lowWatermarkInBpsOfTvl = new uint16[](2); + _lowWatermarkInBpsOfTvl[0] = 100; + _lowWatermarkInBpsOfTvl[1] = 0; + uint256[] memory _bucketCapacity = new uint256[](2); + _bucketCapacity[0] = 2000000000; // 2000 ETH + _bucketCapacity[1] = 5000000000; // 5000 stETH + // Limit to 5000 ETH per day. + // refill rate = 5000 * 1e6 / 86400 = 57870.37 stETH per second. + uint256[] memory _bucketRefillRate = new uint256[](2); + _bucketRefillRate[0] = 23148; + _bucketRefillRate[1] = 57871; + + targets[0] = address(etherFiRedemptionManager); + data[0] = abi.encodeWithSelector(EtherFiRedemptionManager.initializeTokenParameters.selector, _tokens, _exitFeeSplitToTreasuryInBps, _exitFeeInBps, _lowWatermarkInBpsOfTvl, _bucketCapacity, _bucketRefillRate); + + bytes32 timelockSalt = keccak256(abi.encode(targets, data, block.number)); + bytes memory scheduleCalldata = abi.encodeWithSelector( + etherfiOperatingTimelock.scheduleBatch.selector, + targets, + values, + data, + bytes32(0)/*=predecessor*/, + timelockSalt, + MIN_DELAY_OPERATING_TIMELOCK/*=minDelay*/ + ); + + console2.log("Schedule Initialize Token Parameters EFRM Tx:"); + console2.logBytes(scheduleCalldata); + console2.log("================================================"); + console2.log(""); + + bytes memory executeCalldata = abi.encodeWithSelector( + etherfiOperatingTimelock.executeBatch.selector, + targets, + values, + data, + bytes32(0)/*=predecessor*/, + timelockSalt + ); + + console2.log("Execute Initialize Token Parameters EFRM Tx:"); + console2.logBytes(executeCalldata); + console2.log("================================================"); + console2.log(""); + + // uncomment to run against fork + console2.log("=== SCHEDULING BATCH ==="); + etherfiOperatingTimelock.scheduleBatch(targets, values, data, bytes32(0), timelockSalt, MIN_DELAY_OPERATING_TIMELOCK); + console2.log("Schedule of Initialize Token Parameters EFRM Tx successful"); + console2.log("================================================"); + console2.log(""); + + // console2.log("=== FAST FORWARDING TIME ==="); + // vm.warp(block.timestamp + MIN_DELAY_TIMELOCK + 1); // +1 to ensure it's past the delay + // console2.log("New timestamp:", block.timestamp); + // etherfiOperatingTimelock.executeBatch(targets, values, data, bytes32(0), timelockSalt); + // console2.log("Execute of Initialize Token Parameters EFRM Tx successful"); + // console2.log("================================================"); + // console2.log(""); + } + + function upgrade() public { + address[] memory targets = new address[](3); + bytes[] memory data = new bytes[](3); + uint256[] memory values = new uint256[](3); + + targets[0] = address(liquidityPool); + data[0] = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, liquidityPoolImpl); + + targets[1] = address(etherFiRedemptionManager); + data[1] = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, etherFiRedemptionManagerImpl); + + targets[2] = address(etherFiRestaker); + data[2] = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, etherFiRestakerImpl); + + bytes32 timelockSalt = keccak256(abi.encode(targets, data, block.number)); + bytes memory scheduleCalldata = abi.encodeWithSelector( + etherFiTimelock.scheduleBatch.selector, + targets, + values, + data, + bytes32(0)/*=predecessor*/, + timelockSalt, + MIN_DELAY_TIMELOCK/*=minDelay*/ + ); + + console2.log("Schedule Upgrade Tx:"); + console2.logBytes(scheduleCalldata); + console2.log("================================================"); + console2.log(""); + + bytes memory executeCalldata = abi.encodeWithSelector( + etherFiTimelock.executeBatch.selector, + targets, + values, + data, + bytes32(0)/*=predecessor*/, + timelockSalt + ); + + console2.log("Execute Upgrade Tx:"); + console2.logBytes(executeCalldata); + console2.log("================================================"); + console2.log(""); + + // uncomment to run against fork + console2.log("=== SCHEDULING BATCH ==="); + console2.log("Current timestamp:", block.timestamp); + etherFiTimelock.scheduleBatch(targets, values, data, bytes32(0), timelockSalt, MIN_DELAY_TIMELOCK); + console2.log("Schedule of Upgrade Tx successful"); + console2.log("================================================"); + console2.log(""); + + // console2.log("=== FAST FORWARDING TIME ==="); + // vm.warp(block.timestamp + MIN_DELAY_TIMELOCK + 1); // +1 to ensure it's past the delay + // console2.log("New timestamp:", block.timestamp); + // etherFiTimelock.executeBatch(targets, values, data, bytes32(0), timelockSalt); + // console2.log("Execute of Upgrade Tx successful"); + // console2.log("================================================"); + // console2.log(""); + } + + function rollbackUpgrade() public { + address[] memory targets = new address[](3); + bytes[] memory data = new bytes[](3); + uint256[] memory values = new uint256[](3); + + targets[0] = address(liquidityPool); + data[0] = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, oldLiquidityPoolImpl); + + targets[1] = address(etherFiRedemptionManager); + data[1] = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, oldEtherFiRedemptionManagerImpl); + + targets[2] = address(etherFiRestaker); + data[2] = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, oldEtherFiRestakerImpl); + + bytes32 timelockSalt = keccak256(abi.encode(targets, data, block.number)); + bytes memory scheduleCalldata = abi.encodeWithSelector( + etherFiTimelock.scheduleBatch.selector, + targets, + values, + data, + bytes32(0)/*=predecessor*/, + timelockSalt, + MIN_DELAY_TIMELOCK/*=minDelay*/ + ); + + console2.log("Schedule Rollback Upgrade Tx:"); + console2.logBytes(scheduleCalldata); + console2.log("================================================"); + console2.log(""); + + bytes memory executeCalldata = abi.encodeWithSelector( + etherFiTimelock.executeBatch.selector, + targets, + values, + data, + bytes32(0)/*=predecessor*/, + timelockSalt + ); + + console2.log("Execute Rollback Upgrade Tx:"); + console2.logBytes(executeCalldata); + console2.log("================================================"); + console2.log(""); + + // uncomment to run against fork + console2.log("=== SCHEDULING BATCH ==="); + console2.log("Current timestamp:", block.timestamp); + etherFiTimelock.scheduleBatch(targets, values, data, bytes32(0), timelockSalt, MIN_DELAY_TIMELOCK); + console2.log("Schedule of Rollback Upgrade Tx successful"); + console2.log("================================================"); + console2.log(""); + + // console2.log("=== FAST FORWARDING TIME ==="); + // vm.warp(block.timestamp + MIN_DELAY_TIMELOCK + 1); // +1 to ensure it's past the delay + // console2.log("New timestamp:", block.timestamp); + // etherFiTimelock.executeBatch(targets, values, data, bytes32(0), timelockSalt); + // console2.log("Execute of Rollback Upgrade Tx successful"); + // console2.log("================================================"); + // console2.log(""); + } + + function rollbackEFRMStorage() public { + address[] memory targets = new address[](5); + bytes[] memory data = new bytes[](5); + uint256[] memory values = new uint256[](5); + + data[0] = abi.encodeWithSelector(SET_EXIT_FEE_BASIS_POINTS_SELECTOR, oldExitFeeInBps); + data[1] = abi.encodeWithSelector(SET_EXIT_FEE_SPLIT_TO_TREASURY_IN_BPS_SELECTOR, oldExitFeeSplitToTreasuryInBps); + data[2] = abi.encodeWithSelector(SET_LOW_WATERMARK_IN_BPS_OF_TVL_SELECTOR, oldLowWatermarkInBpsOfTvl); + data[3] = abi.encodeWithSelector(SET_REFILL_RATE_PER_SECOND_SELECTOR, oldRefillRatePerSecond); + data[4] = abi.encodeWithSelector(SET_CAPACITY_SELECTOR, oldCapacity); + + for(uint256 i = 0; i < targets.length; i++) { + targets[i] = address(etherFiRedemptionManager); + } + + bytes32 timelockSalt = keccak256(abi.encode(targets, data, block.number)); + bytes memory scheduleCalldata = abi.encodeWithSelector( + etherfiOperatingTimelock.scheduleBatch.selector, + targets, + values, + data, + bytes32(0)/*=predecessor*/, + timelockSalt, + MIN_DELAY_OPERATING_TIMELOCK/*=minDelay*/ + ); + + console2.log("Schedule Rollback EFRM Storage Tx:"); + console2.logBytes(scheduleCalldata); + console2.log("================================================"); + console2.log(""); + + bytes memory executeCalldata = abi.encodeWithSelector( + etherfiOperatingTimelock.executeBatch.selector, + targets, + values, + data, + bytes32(0)/*=predecessor*/, + timelockSalt + ); + + console2.log("Execute Rollback EFRM Storage Tx:"); + console2.logBytes(executeCalldata); + console2.log("================================================"); + console2.log(""); + + // uncomment to run against fork + console2.log("=== SCHEDULING BATCH ==="); + console2.log("Current timestamp:", block.timestamp); + etherfiOperatingTimelock.scheduleBatch(targets, values, data, bytes32(0), timelockSalt, MIN_DELAY_OPERATING_TIMELOCK); + console2.log("Schedule of Rollback EFRM Storage Tx successful"); + console2.log("================================================"); + console2.log(""); + + // console2.log("=== EXECUTING BATCH ==="); + // vm.warp(block.timestamp + MIN_DELAY_OPERATING_TIMELOCK + 1); // +1 to ensure it's past the delay + // console2.log("New timestamp:", block.timestamp); + // etherfiOperatingTimelock.executeBatch(targets, values, data, bytes32(0), timelockSalt); + // console2.log("Execute of Rollback EFRM Storage Tx successful"); + // console2.log("================================================"); + // console2.log(""); + } +} \ No newline at end of file diff --git a/script/stETH-withdrawals/verify.s.sol b/script/stETH-withdrawals/verify.s.sol new file mode 100644 index 00000000..f556c8c0 --- /dev/null +++ b/script/stETH-withdrawals/verify.s.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "forge-std/Script.sol"; +import "forge-std/Test.sol"; +import {Utils} from "../utils/utils.sol"; +import {ContractCodeChecker} from "../ContractCodeChecker.sol"; +import {LiquidityPool} from "../../src/LiquidityPool.sol"; +import {EtherFiRestaker} from "../../src/EtherFiRestaker.sol"; +import {EtherFiRedemptionManagerTemp} from "../../src/EtherFiRedemptionManagerTemp.sol"; +import {EtherFiRedemptionManager} from "../../src/EtherFiRedemptionManager.sol"; +import {RedemptionInfo} from "../../src/EtherFiRedemptionManager.sol"; +import {BucketLimiter} from "lib/BucketLimiter.sol"; +import {ICreate2Factory} from "../utils/utils.sol"; + +contract VerifyStETHWithdrawals is Script, Test, Utils { + bytes32 commitHashSalt = bytes32(bytes20(hex"037da63f453b943e7bd96c155e0798003094e4a0")); + ICreate2Factory constant factory = ICreate2Factory(0x356d1B83970CeF2018F2c9337cDdb67dff5AEF99); + address constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + //-------------------------------------------------------------------------------------- + //---------------------------- New Deployments ----------------------------------------- + //-------------------------------------------------------------------------------------- + + address constant newLiquidityPoolImpl = 0xA5C1ddD9185901E3c05E0660126627E039D0a626; + address constant newEtherFiRedemptionManagerTempImpl = 0x590015FDf9334594B0Ae14f29b0dEd9f1f8504Bc; + address constant newEtherFiRestakerImpl = 0x71bEf55739F0b148E2C3e645FDE947f380C48615; + address constant newEtherFiRedemptionManagerImpl = 0xE3F384Dc7002547Dd240AC1Ad69a430CCE1e292d; + + //-------------------------------------------------------------------------------------- + //------------------------- Existing Users/Proxies ------------------------------------- + //-------------------------------------------------------------------------------------- + // address rewardsCoordinator = 0x7750d328b314EfFa365A0402CcfD489B80B0adda; // Eigen Layer Rewards Coordinator - https://etherscan.io/address/0x7750d328b314effa365a0402ccfd489b80b0adda + // address etherFiRedemptionManager = 0xDadEf1fFBFeaAB4f68A9fD181395F68b4e4E7Ae0; + // address eETH = 0x35fA164735182de50811E8e2E824cFb9B6118ac2; + // address weETH = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee; + // address stETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; + // address liquidityPool = 0x308861A430be4cce5502d0A12724771Fc6DaF216; + // address operatingTimelock = 0xcD425f44758a08BaAB3C4908f3e3dE5776e45d7a; + // address roleRegistry = 0x62247D29B4B9BECf4BB73E0c722cf6445cfC7cE9; + // address treasury = 0x0c83EAe1FE72c390A02E426572854931EefF93BA; + // address etherFiRestaker = 0x1B7a4C3797236A1C37f8741c0Be35c2c72736fFf; + + address REWARDS_COORDINATOR; + address ETHERFI_REDEMPTION_MANAGER_PROXY; + address EETH_PROXY; + address WEETH_PROXY; + address STETH_PROXY; + address LIQUIDITY_POOL_PROXY; + address OPERATING_TIMELOCK; + address ROLE_REGISTRY_PROXY; + address TREASURY_PROXY; + address ETHERFI_RESTAKER_PROXY; + ContractCodeChecker checker; + //-------------------------------------------------------------------------------------- + //----------------------------- OLD EFRM SELECTORS ----------------------------------- + //-------------------------------------------------------------------------------------- + bytes4 constant SET_EXIT_FEE_BASIS_POINTS_SELECTOR = 0xad0cba24; + bytes4 constant SET_EXIT_FEE_SPLIT_TO_TREASURY_IN_BPS_SELECTOR = 0x69b095a2; + bytes4 constant SET_LOW_WATERMARK_IN_BPS_OF_TVL_SELECTOR = 0x298f3f03; + bytes4 constant SET_REFILL_RATE_PER_SECOND_SELECTOR = 0x2f530824; + bytes4 constant SET_CAPACITY_SELECTOR = 0x91915ef8; + + function setUp() public { + checker = new ContractCodeChecker(); + + REWARDS_COORDINATOR = 0x7750d328b314EfFa365A0402CcfD489B80B0adda; + ETHERFI_REDEMPTION_MANAGER_PROXY = 0xDadEf1fFBFeaAB4f68A9fD181395F68b4e4E7Ae0; + EETH_PROXY = 0x35fA164735182de50811E8e2E824cFb9B6118ac2; + WEETH_PROXY = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee; + STETH_PROXY = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; + LIQUIDITY_POOL_PROXY = 0x308861A430be4cce5502d0A12724771Fc6DaF216; + OPERATING_TIMELOCK = 0xcD425f44758a08BaAB3C4908f3e3dE5776e45d7a; + ROLE_REGISTRY_PROXY = 0x62247D29B4B9BECf4BB73E0c722cf6445cfC7cE9; + TREASURY_PROXY = 0x0c83EAe1FE72c390A02E426572854931EefF93BA; + ETHERFI_RESTAKER_PROXY = 0x1B7a4C3797236A1C37f8741c0Be35c2c72736fFf; + } + + function run() external { + + console2.log("========================================"); + console2.log("Starting StETH Withdrawals Verification"); + console2.log("========================================\n"); + + // 0. Verify addresses + console2.log("0. VERIFYING ADDRESSES"); + console2.log("----------------------------------------"); + verifyAddresses(); + + // 1. Verify bytecode match + console2.log("1. VERIFYING BYTECODE MATCH"); + console2.log("----------------------------------------"); + verifyBytecode(); + + // 2. Verify Upgradeability for Implementations + console2.log("2. VERIFYING UPGRADEABILITY"); + console2.log("----------------------------------------"); + verifyUpgradeability(); + console2.log(""); + + // 3. Verify New Functionality + console2.log("3. VERIFYING NEW FUNCTIONALITY"); + console2.log("----------------------------------------"); + verifyNewFunctionality(); + + console2.log("========================================"); + console2.log("All Verifications Passed"); + console2.log("========================================"); + } + + function verifyUpgradeability() public { + // ──────────────────────────────────────────────────────────────────────────── + // Individual proxy checks + // ──────────────────────────────────────────────────────────────────────────── + verifyProxyUpgradeability(address(LIQUIDITY_POOL_PROXY), "LiquidityPool"); + verifyProxyUpgradeability(address(ETHERFI_RESTAKER_PROXY), "EtherFiRestaker"); + verifyProxyUpgradeability(address(ETHERFI_REDEMPTION_MANAGER_PROXY), "EtherFiRedemptionManager"); + } + + function verifyBytecode() public { + LiquidityPool liquidityPoolImplementation = new LiquidityPool(); + EtherFiRestaker etherFiRestakerImplementation = new EtherFiRestaker(address(REWARDS_COORDINATOR), address(ETHERFI_REDEMPTION_MANAGER_PROXY)); + EtherFiRedemptionManagerTemp etherFiRedemptionManagerTempImplementation = new EtherFiRedemptionManagerTemp(address(LIQUIDITY_POOL_PROXY), address(EETH_PROXY), address(WEETH_PROXY), address(TREASURY_PROXY), address(ROLE_REGISTRY_PROXY)); + EtherFiRedemptionManager etherFiRedemptionManagerImplementation = new EtherFiRedemptionManager(address(LIQUIDITY_POOL_PROXY), address(EETH_PROXY), address(WEETH_PROXY), address(TREASURY_PROXY), address(ROLE_REGISTRY_PROXY), address(ETHERFI_RESTAKER_PROXY)); + + console2.log(unicode"✓ Checking Bytecode for LiquidityPool:", address(newLiquidityPoolImpl)); + console2.log("liquidityPoolImplementation:", address(liquidityPoolImplementation)); + checker.verifyContractByteCodeMatch(address(newLiquidityPoolImpl), address(liquidityPoolImplementation)); + console2.log("----------------------------------------"); + console2.log(unicode"✓ Checking Bytecode for EtherFiRestaker:", address(newEtherFiRestakerImpl)); + console2.log("etherFiRestakerImplementation:", address(etherFiRestakerImplementation)); + checker.verifyContractByteCodeMatch(address(newEtherFiRestakerImpl), address(etherFiRestakerImplementation)); + console2.log("----------------------------------------"); + console2.log(unicode"✓ Checking Bytecode for EtherFiRedemptionManager:", address(newEtherFiRedemptionManagerImpl)); + console2.log("etherFiRedemptionManagerImplementation:", address(etherFiRedemptionManagerImplementation)); + checker.verifyContractByteCodeMatch(address(newEtherFiRedemptionManagerImpl), address(etherFiRedemptionManagerImplementation)); + console2.log("----------------------------------------"); + console2.log(unicode"✓ Checking Bytecode for EtherFiRedemptionManagerTemp:", address(newEtherFiRedemptionManagerTempImpl)); + console2.log("etherFiRedemptionManagerTempImplementation:", address(etherFiRedemptionManagerTempImplementation)); + checker.verifyContractByteCodeMatch(address(newEtherFiRedemptionManagerTempImpl), address(etherFiRedemptionManagerTempImplementation)); + } + + function verifyAddresses() public { + address liquidityPoolImplementation; + address etherFiRestakerImplementation; + address etherFiRedemptionManagerImplementation; + address etherFiRedemptionManagerTempImplementation; + + // EtherFiRedemptionManager + { + string memory contractName = "EtherFiRedemptionManagerTemp"; + bytes memory constructorArgs = abi.encode( + address(LIQUIDITY_POOL_PROXY), + address(EETH_PROXY), + address(WEETH_PROXY), + address(TREASURY_PROXY), + address(ROLE_REGISTRY_PROXY) + ); + bytes memory bytecode = abi.encodePacked( + type(EtherFiRedemptionManagerTemp).creationCode, + constructorArgs + ); + etherFiRedemptionManagerTempImplementation = verifyCreate2Address(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); + } + + // EtherFiRestaker + { + string memory contractName = "EtherFiRestaker"; + bytes memory constructorArgs = abi.encode( + address(REWARDS_COORDINATOR), + address(ETHERFI_REDEMPTION_MANAGER_PROXY) + ); + bytes memory bytecode = abi.encodePacked( + type(EtherFiRestaker).creationCode, + constructorArgs + ); + etherFiRestakerImplementation = verifyCreate2Address(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); + } + + // EtherFiRedemptionManager + { + string memory contractName = "EtherFiRedemptionManager"; + bytes memory constructorArgs = abi.encode( + address(LIQUIDITY_POOL_PROXY), + address(EETH_PROXY), + address(WEETH_PROXY), + address(TREASURY_PROXY), + address(ROLE_REGISTRY_PROXY), + address(ETHERFI_RESTAKER_PROXY) + ); + bytes memory bytecode = abi.encodePacked( + type(EtherFiRedemptionManager).creationCode, + constructorArgs + ); + etherFiRedemptionManagerImplementation = verifyCreate2Address(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); + } + + // LiquidityPool + { + string memory contractName = "LiquidityPool"; + bytes memory constructorArgs = abi.encode( + ); + bytes memory bytecode = abi.encodePacked( + type(LiquidityPool).creationCode, + constructorArgs + ); + liquidityPoolImplementation = verifyCreate2Address(contractName, constructorArgs, bytecode, commitHashSalt, true, factory); + } + + assertEq(liquidityPoolImplementation, newLiquidityPoolImpl); + assertEq(etherFiRestakerImplementation, newEtherFiRestakerImpl); + assertEq(etherFiRedemptionManagerImplementation, newEtherFiRedemptionManagerImpl); + assertEq(etherFiRedemptionManagerTempImplementation, newEtherFiRedemptionManagerTempImpl); + + console2.log("----------------------------------------"); + console2.log("All Addresses Verified"); + console2.log("----------------------------------------"); + } + + function verifyNewFunctionality() public { + // ──────────────────────────────────────────────────────────────────────────── + // Verify new functionality + // ──────────────────────────────────────────────────────────────────────────── + + LiquidityPool liquidityPoolInstance = LiquidityPool(payable(LIQUIDITY_POOL_PROXY)); + bytes4 selector1 = liquidityPoolInstance.burnEEthSharesForNonETHWithdrawal.selector; + console2.log(unicode"✓ burnEEthSharesForNonETHWithdrawal exists:", vm.toString(selector1)); + + EtherFiRestaker etherFiRestakerInstance = EtherFiRestaker(payable(ETHERFI_RESTAKER_PROXY)); + bytes4 selector2 = etherFiRestakerInstance.transferStETH.selector; + console2.log(unicode"✓ transferStETH exists:", vm.toString(selector2)); + + EtherFiRedemptionManager etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(ETHERFI_REDEMPTION_MANAGER_PROXY)); + (BucketLimiter.Limit memory limit, uint16 exitFeeSplitToTreasuryInBps, uint16 exitFeeInBps, uint16 lowWatermarkInBpsOfTvl) = etherFiRedemptionManagerInstance.tokenToRedemptionInfo(address(ETH)); + console2.log(unicode"✓ exitFeeSplitToTreasuryInBps exists:", exitFeeSplitToTreasuryInBps); + console2.log(unicode"✓ exitFeeInBps exists:", exitFeeInBps); + console2.log(unicode"✓ lowWatermarkInBpsOfTvl exists:", lowWatermarkInBpsOfTvl); + + (BucketLimiter.Limit memory limit2, uint16 exitFeeSplitToTreasuryInBps2, uint16 exitFeeInBps2, uint16 lowWatermarkInBpsOfTvl2) = etherFiRedemptionManagerInstance.tokenToRedemptionInfo(address(STETH_PROXY)); + console2.log(unicode"✓ exitFeeSplitToTreasuryInBps2 exists:", exitFeeSplitToTreasuryInBps2); + console2.log(unicode"✓ exitFeeInBps2 exists:", exitFeeInBps2); + console2.log(unicode"✓ lowWatermarkInBpsOfTvl2 exists:", lowWatermarkInBpsOfTvl2); + + assertNotEq(etherFiRedemptionManagerInstance.setExitFeeBasisPoints.selector, SET_EXIT_FEE_BASIS_POINTS_SELECTOR); + assertNotEq(etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps.selector, SET_EXIT_FEE_SPLIT_TO_TREASURY_IN_BPS_SELECTOR); + assertNotEq(etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl.selector, SET_LOW_WATERMARK_IN_BPS_OF_TVL_SELECTOR); + assertNotEq(etherFiRedemptionManagerInstance.setRefillRatePerSecond.selector, SET_REFILL_RATE_PER_SECOND_SELECTOR); + assertNotEq(etherFiRedemptionManagerInstance.setCapacity.selector, SET_CAPACITY_SELECTOR); + } +} \ No newline at end of file diff --git a/script/utils/utils.sol b/script/utils/utils.sol new file mode 100644 index 00000000..c3ed3fbb --- /dev/null +++ b/script/utils/utils.sol @@ -0,0 +1,452 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Script.sol"; +import "forge-std/Vm.sol"; +import "forge-std/StdJson.sol"; +import "forge-std/console.sol"; +import "forge-std/console2.sol"; +import "openzeppelin-contracts-upgradeable/contracts/interfaces/draft-IERC1822Upgradeable.sol"; + +interface ICreate2Factory { + function deploy(bytes memory code, bytes32 salt) external payable returns (address); + function verify(address addr, bytes32 salt, bytes memory code) external view returns (bool); + function computeAddress(bytes32 salt, bytes memory code) external view returns (address); +} + +interface IUpgrade { + function upgradeTo(address) external; + + function roleRegistry() external returns (address); +} + +contract Utils is Script { + // ERC1967 storage slot for implementation address + bytes32 constant IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + uint256 MIN_DELAY_OPERATING_TIMELOCK = 28800; // 8 hours + uint256 MIN_DELAY_TIMELOCK = 259200; // 72 hours + + function deploy(string memory contractName, bytes memory constructorArgs, bytes memory bytecode, bytes32 salt, bool logging, ICreate2Factory factory) internal returns (address) { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address predictedAddress = factory.computeAddress(salt, bytecode); + address deployedAddress = factory.deploy(bytecode, salt); + require(deployedAddress == predictedAddress, "Deployment address mismatch"); + + if (logging) { + + // 5. Create JSON deployment log (exact same format) + string memory deployLog = string.concat( + "{\n", + ' "contractName": "', contractName, '",\n', + ' "deploymentParameters": {\n', + ' "factory": "', vm.toString(address(factory)), '",\n', + ' "salt": "', vm.toString(salt), '",\n', + formatConstructorArgs(constructorArgs, contractName), '\n', + ' },\n', + ' "deployedAddress": "', vm.toString(deployedAddress), '"\n', + "}" + ); + + // 6. Save deployment log + string memory root = vm.projectRoot(); + string memory logFileDir = string.concat(root, "/deployment/", contractName); + vm.createDir(logFileDir, true); + + string memory logFileName = string.concat( + logFileDir, + "/", + getTimestampString(), + ".json" + ); + vm.writeFile(logFileName, deployLog); + + // 7. Console output + console.log("\n=== Deployment Successful ==="); + console.log("Contract:", contractName); + console.log("Deployed to:", deployedAddress); + console.log("Deployment log saved to:", logFileName); + console.log(deployLog); + } + + return deployedAddress; + } + + function verify(address addr, bytes memory bytecode, bytes32 salt, ICreate2Factory factory) internal view returns (bool) { + return factory.verify(addr, salt, bytecode); + } + + function getImplementation(address proxy) internal view returns (address) { + bytes32 slot = IMPLEMENTATION_SLOT; + bytes32 value = vm.load(proxy, slot); + return address(uint160(uint256(value))); + } + + function checkCondition(bool condition, string memory message) internal pure { + require(condition, message); + } + + function verifyProxyUpgradeability( + address proxy, + string memory name + ) internal { + console2.log(string.concat("Checking ", name, " upgradeability...")); + + // 1. Proxy really points to an implementation + address impl = getImplementation(proxy); + console.log("Implementation:", impl); + checkCondition( + impl != address(0) && impl != proxy, + string.concat(name, " is a proxy with an implementation") + ); + + // 2. Implementation exposes correct proxiableUUID() + try IERC1822ProxiableUpgradeable(impl).proxiableUUID() returns ( + bytes32 slot + ) { + checkCondition( + slot == IMPLEMENTATION_SLOT, + string.concat(name, " implementation returns correct UUID") + ); + } catch { + checkCondition( + false, + string.concat(name, " implementation missing proxiableUUID()") + ); + } + + (bool ok, bytes memory data) = proxy.staticcall( + abi.encodeWithSignature("upgradeTo(address)", impl) + ); + + checkCondition( + ok || data.length != 0, + string.concat(name, " proxy exposes upgradeTo()") + ); + vm.prank(address(0xcaffe)); + try IUpgrade(proxy).upgradeTo(address(0xbeef)) { + checkCondition( + false, + string.concat(name, " allows a random to upgrade") + ); + } catch { + checkCondition( + true, + string.concat(name, " does not allows a random to upgrade") + ); + } + } + + // Helper function to verify Create2 address + function verifyCreate2Address( + string memory contractName, + bytes memory constructorArgs, + bytes memory bytecode, + bytes32 salt, + bool logging, + ICreate2Factory factory + ) internal view returns (address) { + address predictedAddress = factory.computeAddress(salt, bytecode); + return predictedAddress; + } + + //------------------------------------------------------------------------- + // Parse and format constructor arguments into JSON + //------------------------------------------------------------------------- + + function formatConstructorArgs(bytes memory constructorArgs, string memory contractName) + internal + view + returns (string memory) + { + // 1. Load artifact JSON + string memory artifactJson = readArtifact(contractName); + + // 2. Parse ABI inputs for the constructor + bytes memory inputsArray = vm.parseJson(artifactJson, "$.abi[?(@.type == 'constructor')].inputs"); + if (inputsArray.length == 0) { + // No constructor, return empty object + return ' "constructorArgs": {}'; + } + + // 3. Decode to get the number of inputs + bytes[] memory decodedInputs = abi.decode(inputsArray, (bytes[])); + uint256 inputCount = decodedInputs.length; + + // 4. Collect param names and types in arrays + (string[] memory names, string[] memory typesArr) = getConstructorMetadata(artifactJson, inputCount); + + // 5. Build the final JSON + return decodeParamsJson(constructorArgs, names, typesArr); + } + + /** + * @dev Helper to read the contract's compiled artifact + */ + function readArtifact(string memory contractName) internal view returns (string memory) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/out/", contractName, ".sol/", contractName, ".json"); + return vm.readFile(path); + } + + /** + * @dev Extracts all `name` and `type` fields from the constructor inputs + */ + function getConstructorMetadata(string memory artifactJson, uint256 inputCount) + internal + pure + returns (string[] memory, string[] memory) + { + string[] memory names = new string[](inputCount); + string[] memory typesArr = new string[](inputCount); + + for (uint256 i = 0; i < inputCount; i++) { + // We'll build the JSON path e.g. "$.abi[?(@.type == 'constructor')].inputs[0].name" + string memory baseQuery = string.concat("$.abi[?(@.type == 'constructor')].inputs[", vm.toString(i), "]"); + + names[i] = trim(string(vm.parseJson(artifactJson, string.concat(baseQuery, ".name")))); + typesArr[i] = trim(string(vm.parseJson(artifactJson, string.concat(baseQuery, ".type")))); + } + return (names, typesArr); + } + + /** + * @dev Decodes each provided constructorArg and builds the JSON lines + */ + function decodeParamsJson( + bytes memory constructorArgs, + string[] memory names, + string[] memory typesArr + ) + internal + pure + returns (string memory) + { + uint256 offset; + string memory json = ' "constructorArgs": {\n'; + + for (uint256 i = 0; i < names.length; i++) { + (string memory val, uint256 newOffset) = decodeParam(constructorArgs, offset, typesArr[i]); + offset = newOffset; + + json = string.concat( + json, + ' "', names[i], '": "', val, '"', + (i < names.length - 1) ? ",\n" : "\n" + ); + } + return string.concat(json, " }"); + } + + //------------------------------------------------------------------------- + // Decoder logic (same as before) + //------------------------------------------------------------------------- + + function decodeParam(bytes memory data, uint256 offset, string memory t) + internal + pure + returns (string memory, uint256) + { + if (!isDynamicType(t)) { + // For static params, read 32 bytes directly + bytes memory chunk = slice(data, offset, 32); + return (formatStaticParam(t, bytes32(chunk)), offset + 32); + } else { + // Dynamic param: first 32 bytes is a pointer to the data location + uint256 dataLoc = uint256(bytes32(slice(data, offset, 32))); + offset += 32; + + // Next 32 bytes at that location is the length + uint256 len = uint256(bytes32(slice(data, dataLoc, 32))); + bytes memory dynData = slice(data, dataLoc + 32, len); + + return (formatDynamicParam(t, dynData), offset); + } + } + + function formatStaticParam(string memory t, bytes32 chunk) internal pure returns (string memory) { + if (compare(t, "address")) { + return vm.toString(address(uint160(uint256(chunk)))); + } else if (compare(t, "uint256")) { + return vm.toString(uint256(chunk)); + } else if (compare(t, "bool")) { + return uint256(chunk) != 0 ? "true" : "false"; + } else if (compare(t, "bytes32")) { + return vm.toString(chunk); + } + revert("Unsupported static type"); + } + + function formatDynamicParam(string memory t, bytes memory dynData) internal pure returns (string memory) { + if (compare(t, "string")) { + return string(dynData); + } else if (compare(t, "bytes")) { + return vm.toString(dynData); + } else if (endsWithArray(t)) { + // e.g. "uint256[]" or "address[]" + if (startsWith(t, "uint256")) { + uint256[] memory arr = abi.decode(dynData, (uint256[])); + return formatUint256Array(arr); + } else if (startsWith(t, "address")) { + address[] memory arr = abi.decode(dynData, (address[])); + return formatAddressArray(arr); + } + } + revert("Unsupported dynamic type"); + } + + //------------------------------------------------------------------------- + // Array format helpers + //------------------------------------------------------------------------- + + function formatUint256Array(uint256[] memory arr) internal pure returns (string memory) { + string memory out = "["; + for (uint256 i = 0; i < arr.length; i++) { + out = string.concat(out, (i == 0 ? "" : ","), vm.toString(arr[i])); + } + return string.concat(out, "]"); + } + + function formatAddressArray(address[] memory arr) internal pure returns (string memory) { + string memory out = "["; + for (uint256 i = 0; i < arr.length; i++) { + out = string.concat(out, (i == 0 ? "" : ","), vm.toString(arr[i])); + } + return string.concat(out, "]"); + } + + //------------------------------------------------------------------------- + // Type checks + //------------------------------------------------------------------------- + + function isDynamicType(string memory t) internal pure returns (bool) { + return startsWith(t, "string") || startsWith(t, "bytes") || endsWithArray(t); + } + + function endsWithArray(string memory t) internal pure returns (bool) { + bytes memory b = bytes(t); + return b.length >= 2 && (b[b.length - 2] == '[' && b[b.length - 1] == ']'); + } + + //------------------------------------------------------------------------- + // Low-level bytes slicing + //------------------------------------------------------------------------- + + function slice(bytes memory data, uint256 start, uint256 length) internal pure returns (bytes memory) { + require(data.length >= start + length, "slice_outOfBounds"); + bytes memory out = new bytes(length); + for (uint256 i = 0; i < length; i++) { + out[i] = data[start + i]; + } + return out; + } + + //------------------------------------------------------------------------- + // String helpers + //------------------------------------------------------------------------- + + function trim(string memory str) internal pure returns (string memory) { + bytes memory b = bytes(str); + uint256 start; + uint256 end = b.length; + while (start < b.length && uint8(b[start]) <= 0x20) start++; + while (end > start && uint8(b[end - 1]) <= 0x20) end--; + bytes memory out = new bytes(end - start); + for (uint256 i = 0; i < out.length; i++) { + out[i] = b[start + i]; + } + return string(out); + } + + function compare(string memory a, string memory b) internal pure returns (bool) { + return keccak256(bytes(a)) == keccak256(bytes(b)); + } + + function startsWith(string memory str, string memory prefix) internal pure returns (bool) { + bytes memory s = bytes(str); + bytes memory p = bytes(prefix); + if (s.length < p.length) return false; + for (uint256 i = 0; i < p.length; i++) { + if (s[i] != p[i]) return false; + } + return true; + } + + //------------------------------------------------------------------------- + // Timestamp-based filename + //------------------------------------------------------------------------- + + // The timestamp is in UTC (Coordinated Universal Time). This is because block.timestamp returns a Unix timestamp, which is always in UTC. + function getTimestampString() internal view returns (string memory) { + uint256 ts = block.timestamp; + + // Calculate year accounting for leap years + uint256 year = 1970; + uint256 remainingSeconds = ts; + + while (remainingSeconds >= secondsInYear(year)) { + remainingSeconds -= secondsInYear(year); + year++; + } + + // Calculate month accounting for varying month lengths + uint256 month = 1; + while (remainingSeconds >= secondsInMonth(year, month)) { + remainingSeconds -= secondsInMonth(year, month); + month++; + } + + // Calculate day (1-based) + uint256 day = (remainingSeconds / 86400) + 1; + remainingSeconds %= 86400; + + // Calculate time components + uint256 hour = remainingSeconds / 3600; + remainingSeconds %= 3600; + uint256 minute = remainingSeconds / 60; + uint256 second = remainingSeconds % 60; + + return string.concat( + vm.toString(year), "-", + pad(vm.toString(month)), "-", + pad(vm.toString(day)), "-", + pad(vm.toString(hour)), "-", + pad(vm.toString(minute)), "-", + pad(vm.toString(second)) + ); + } + + // Helper function to calculate seconds in a given year (accounting for leap years) + function secondsInYear(uint256 year) internal pure returns (uint256) { + if (isLeapYear(year)) { + return 366 * 86400; // 366 days * 24 hours * 3600 seconds + } else { + return 365 * 86400; // 365 days * 24 hours * 3600 seconds + } + } + + // Helper function to calculate seconds in a given month (accounting for varying month lengths) + function secondsInMonth(uint256 year, uint256 month) internal pure returns (uint256) { + uint256 daysInMonth; + + if (month == 2) { + daysInMonth = isLeapYear(year) ? 29 : 28; + } else if (month == 4 || month == 6 || month == 9 || month == 11) { + daysInMonth = 30; + } else { + daysInMonth = 31; + } + + return daysInMonth * 86400; // days * 24 hours * 3600 seconds + } + + // Helper function to determine if a year is a leap year + function isLeapYear(uint256 year) internal pure returns (bool) { + return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); + } + + function pad(string memory n) internal pure returns (string memory) { + return bytes(n).length == 1 ? string.concat("0", n) : n; + } +} \ No newline at end of file diff --git a/src/EtherFiRedemptionManager.sol b/src/EtherFiRedemptionManager.sol index 821f41df..0c1a3793 100644 --- a/src/EtherFiRedemptionManager.sol +++ b/src/EtherFiRedemptionManager.sol @@ -16,16 +16,26 @@ import "@openzeppelin/contracts/utils/math/Math.sol"; import "./interfaces/ILiquidityPool.sol"; import "./interfaces/IeETH.sol"; import "./interfaces/IWeETH.sol"; +import "./interfaces/ILiquifier.sol"; +import "./EtherFiRestaker.sol"; import "lib/BucketLimiter.sol"; import "./RoleRegistry.sol"; /* - The contract allows instant redemption of eETH and weETH tokens to ETH with an exit fee. + The contract allows instant redemption of eETH and weETH tokens to ETH or stETH with an exit fee. - It has the exit fee as a percentage of the total amount redeemed. - It has a rate limiter to limit the total amount that can be redeemed in a given time period. */ + +struct RedemptionInfo { + BucketLimiter.Limit limit; + uint16 exitFeeSplitToTreasuryInBps; + uint16 exitFeeInBps; + uint16 lowWatermarkInBpsOfTvl; +} + contract EtherFiRedemptionManager is Initializable, PausableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { using SafeERC20 for IERC20; using Math for uint256; @@ -34,29 +44,36 @@ contract EtherFiRedemptionManager is Initializable, PausableUpgradeable, Reentra uint256 private constant BASIS_POINT_SCALE = 1e4; bytes32 public constant ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE = keccak256("ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE"); + address public constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; RoleRegistry public immutable roleRegistry; address public immutable treasury; IeETH public immutable eEth; IWeETH public immutable weEth; ILiquidityPool public immutable liquidityPool; + EtherFiRestaker public immutable etherFiRestaker; + ILido public immutable lido; + + mapping(address => RedemptionInfo) public tokenToRedemptionInfo; + + + event Redeemed(address indexed receiver, uint256 redemptionAmount, uint256 feeAmountToTreasury, uint256 feeAmountToStakers, address token); - BucketLimiter.Limit public limit; - uint16 public exitFeeSplitToTreasuryInBps; - uint16 public exitFeeInBps; - uint16 public lowWatermarkInBpsOfTvl; // bps of TVL + error InvalidAmount(); + error InvalidOutputToken(); - event Redeemed(address indexed receiver, uint256 redemptionAmount, uint256 feeAmountToTreasury, uint256 feeAmountToStakers); receive() external payable {} /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address _liquidityPool, address _eEth, address _weEth, address _treasury, address _roleRegistry) { + constructor(address _liquidityPool, address _eEth, address _weEth, address _treasury, address _roleRegistry, address _etherFiRestaker) { roleRegistry = RoleRegistry(_roleRegistry); treasury = _treasury; liquidityPool = ILiquidityPool(payable(_liquidityPool)); eEth = IeETH(_eEth); weEth = IWeETH(_weEth); + etherFiRestaker = EtherFiRestaker(payable(_etherFiRestaker)); + lido = etherFiRestaker.lido(); _disableInitializers(); } @@ -69,66 +86,72 @@ contract EtherFiRedemptionManager is Initializable, PausableUpgradeable, Reentra __UUPSUpgradeable_init(); __Pausable_init(); __ReentrancyGuard_init(); + } - limit = BucketLimiter.create(_convertToBucketUnit(_bucketCapacity, Math.Rounding.Down), _convertToBucketUnit(_bucketRefillRate, Math.Rounding.Down)); - exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; - exitFeeInBps = _exitFeeInBps; - lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; + function initializeTokenParameters(address[] memory _tokens, uint16[] memory _exitFeeSplitToTreasuryInBps, uint16[] memory _exitFeeInBps, uint16[] memory _lowWatermarkInBpsOfTvl, uint256[] memory _bucketCapacity, uint256[] memory _bucketRefillRate) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { + for(uint256 i = 0; i < _exitFeeSplitToTreasuryInBps.length; i++) { + require(_exitFeeSplitToTreasuryInBps[i] <= BASIS_POINT_SCALE, "INVALID"); + require(_exitFeeInBps[i] <= BASIS_POINT_SCALE, "INVALID"); + require(_lowWatermarkInBpsOfTvl[i] <= BASIS_POINT_SCALE, "INVALID"); + tokenToRedemptionInfo[address(_tokens[i])] = RedemptionInfo({ + limit: BucketLimiter.create(_convertToBucketUnit(_bucketCapacity[i], Math.Rounding.Down), _convertToBucketUnit(_bucketRefillRate[i], Math.Rounding.Down)), + exitFeeSplitToTreasuryInBps: _exitFeeSplitToTreasuryInBps[i], + exitFeeInBps: _exitFeeInBps[i], + lowWatermarkInBpsOfTvl: _lowWatermarkInBpsOfTvl[i] + }); + } } /** - * @notice Redeems eETH for ETH. + * @notice Redeems eETH for outputToken (ETH or stETH). * @param eEthAmount The amount of eETH to redeem after the exit fee. - * @param receiver The address to receive the redeemed ETH. + * @param receiver The address to receive the redeemed outputToken. + * @param outputToken The token to redeem to (ETH or stETH). */ - function redeemEEth(uint256 eEthAmount, address receiver) public whenNotPaused nonReentrant { - _redeemEEth(eEthAmount, receiver); + function redeemEEth(uint256 eEthAmount, address receiver, address outputToken) public whenNotPaused nonReentrant { + _redeemEEth(eEthAmount, receiver, outputToken); } /** - * @notice Redeems weETH for ETH. + * @notice Redeems weETH for outputToken (ETH or stETH). * @param weEthAmount The amount of weETH to redeem after the exit fee. - * @param receiver The address to receive the redeemed ETH. + * @param receiver The address to receive the redeemed outputToken. + * @param outputToken The token to redeem to (ETH or stETH). */ - function redeemWeEth(uint256 weEthAmount, address receiver) public whenNotPaused nonReentrant { - _redeemWeEth(weEthAmount, receiver); + function redeemWeEth(uint256 weEthAmount, address receiver, address outputToken) public whenNotPaused nonReentrant { + _redeemWeEth(weEthAmount, receiver, outputToken); } /** - * @notice Redeems eETH for ETH with permit. + * @notice Redeems eETH for outputToken (ETH or stETH) with permit. * @param eEthAmount The amount of eETH to redeem after the exit fee. - * @param receiver The address to receive the redeemed ETH. + * @param receiver The address to receive the redeemed outputToken. * @param permit The permit params. + * @param outputToken The token to redeem to (ETH or stETH). */ - function redeemEEthWithPermit(uint256 eEthAmount, address receiver, IeETH.PermitInput calldata permit) external whenNotPaused nonReentrant { + function redeemEEthWithPermit(uint256 eEthAmount, address receiver, IeETH.PermitInput calldata permit, address outputToken) external whenNotPaused nonReentrant { try eEth.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} - _redeemEEth(eEthAmount, receiver); + _redeemEEth(eEthAmount, receiver, outputToken); } /** - * @notice Redeems weETH for ETH. + * @notice Redeems weETH for outputToken (ETH or stETH). * @param weEthAmount The amount of weETH to redeem after the exit fee. - * @param receiver The address to receive the redeemed ETH. + * @param receiver The address to receive the redeemed outputToken. * @param permit The permit params. + * @param outputToken The token to redeem to (ETH or stETH). */ - function redeemWeEthWithPermit(uint256 weEthAmount, address receiver, IWeETH.PermitInput calldata permit) external whenNotPaused nonReentrant { + function redeemWeEthWithPermit(uint256 weEthAmount, address receiver, IWeETH.PermitInput calldata permit, address outputToken) external whenNotPaused nonReentrant { try weEth.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} - _redeemWeEth(weEthAmount, receiver); + _redeemWeEth(weEthAmount, receiver, outputToken); } - /** - * @notice Redeems ETH. - * @param ethAmount The amount of ETH to redeem after the exit fee. - * @param receiver The address to receive the redeemed ETH. - */ - function _redeem(uint256 ethAmount, uint256 eEthShares, address receiver, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) internal { - _updateRateLimit(ethAmount); - - // Derive additionals - uint256 eEthShareFee = eEthShares - sharesToBurn; - uint256 feeShareToStakers = eEthShareFee - feeShareToTreasury; - - // Snapshot balances & shares for sanity check at the end + function _processETHRedemption( + address receiver, + uint256 eEthAmountToReceiver, + uint256 sharesToBurn, + uint256 feeShareToStakers + ) internal { uint256 prevBalance = address(this).balance; uint256 prevLpBalance = address(liquidityPool).balance; uint256 totalEEthShare = eEth.totalShares(); @@ -139,11 +162,6 @@ contract EtherFiRedemptionManager is Initializable, PausableUpgradeable, Reentra // To Stakers by burning shares liquidityPool.burnEEthShares(feeShareToStakers); - - // To Treasury by transferring eETH - IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); - - // uint256 totalShares = eEth.totalShares(); require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); // To Receiver by transferring ETH, using gas 10k for additional safety @@ -152,82 +170,157 @@ contract EtherFiRedemptionManager is Initializable, PausableUpgradeable, Reentra // Make sure the liquidity pool balance is correct && total shares are correct require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); - // require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); + } + + /** + * @notice Processes stETH-specific redemption logic. + */ + function _processStETHRedemption( + address receiver, + uint256 stEthAmountToReceiver, + uint256 sharesToBurn, + uint256 feeShareToStakers + ) internal { + uint256 eEthAmountToReceiver = stEthAmountToReceiver; // 1 stETH = 1 eETH + if (eEthAmountToReceiver > type(uint128).max || eEthAmountToReceiver == 0 || sharesToBurn == 0) revert InvalidAmount(); + uint256 totalEEthShare = eEth.totalShares(); + uint256 totalValueOutOfLpBefore = liquidityPool.totalValueOutOfLp(); - emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); + // Burn shares for non ETH withdrawal (stETH) + // - sharesToBurn: eETH shares to burn for withdrawal + // - feeShareToStakers: eETH shares to burn for stakers + liquidityPool.burnEEthSharesForNonETHWithdrawal(sharesToBurn, eEthAmountToReceiver); + liquidityPool.burnEEthShares(feeShareToStakers); + + // Validate total shares and total value out of lp + require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); + require(liquidityPool.totalValueOutOfLp() == totalValueOutOfLpBefore - eEthAmountToReceiver, "EtherFiRedemptionManager: Invalid total value out of lp"); + + etherFiRestaker.transferStETH(receiver, eEthAmountToReceiver); + } + + /** + * @notice Redeems outputToken (ETH or stETH). + * The receiver will receive the ETH or stETH after the exit fee. + * The fee will be split between the treasury and the stakers. + * - the portion to the treasury will be transferred to the treasury in eETH. + * - the portion to the stakers will be distributed by burning eETH shares. + * @param ethAmount The amount of outputToken to redeem after the exit fee. + * @param eEthShares The total amount of eETH shares corresponding to the `ethAmount` (= liquidityPool.sharesForAmount(ethAmount)) + * @param eEthAmountToReceiver The amount of ETH or stETH to receiver. + * @param eEthFeeAmountToTreasury The amount of eETH to treasury. + * @param sharesToBurn The amount of eETH shares to burn. + * @param feeShareToTreasury The amount of eETH to treasury. + * @param outputToken The token to redeem (ETH or stETH). + * @param receiver The address to receive the redeemed outputToken. + */ + function _redeem(uint256 ethAmount, uint256 eEthShares, address receiver, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury, address outputToken) internal { + _updateRateLimit(ethAmount, outputToken); + uint256 eEthShareFee = eEthShares - sharesToBurn; + uint256 feeShareToStakers = eEthShareFee - feeShareToTreasury; + + if(outputToken == ETH_ADDRESS) { + _processETHRedemption(receiver, eEthAmountToReceiver, sharesToBurn, feeShareToStakers); + } else if(outputToken == address(lido)) { + _processStETHRedemption(receiver, eEthAmountToReceiver, sharesToBurn, feeShareToStakers); + } else { + revert InvalidOutputToken(); + } + // Common fee handling: Transfer to Treasury + IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); + + emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver, outputToken); } /** * @dev if the contract has less than the low watermark, it will not allow any instant redemption. */ - function lowWatermarkInETH() public view returns (uint256) { - return liquidityPool.getTotalPooledEther().mulDiv(lowWatermarkInBpsOfTvl, BASIS_POINT_SCALE); + function lowWatermarkInETH(address token) public view returns (uint256) { + return liquidityPool.getTotalPooledEther().mulDiv(tokenToRedemptionInfo[token].lowWatermarkInBpsOfTvl, BASIS_POINT_SCALE); + } + + function getInstantLiquidityAmount(address token) public view returns (uint256) { + if(token == ETH_ADDRESS) { + return address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); + } else if (token == address(lido)) { + return lido.balanceOf(address(etherFiRestaker)); + } } /** * @dev Returns the total amount that can be redeemed. */ - function totalRedeemableAmount() external view returns (uint256) { - uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); - if (liquidEthAmount < lowWatermarkInETH()) { + function totalRedeemableAmount(address token) external view returns (uint256) { + uint256 liquidEthAmount = getInstantLiquidityAmount(token); + + if (liquidEthAmount < lowWatermarkInETH(token)) { return 0; } - uint64 consumableBucketUnits = BucketLimiter.consumable(limit); + uint64 consumableBucketUnits = BucketLimiter.consumable(tokenToRedemptionInfo[token].limit); uint256 consumableAmount = _convertFromBucketUnit(consumableBucketUnits); return Math.min(consumableAmount, liquidEthAmount); } /** * @dev Returns whether the given amount can be redeemed. - * @param amount The ETH or eETH amount to check. + * @param amount The ETH or stETH amount to check + * @param token The token to check to redeem */ - function canRedeem(uint256 amount) public view returns (bool) { - uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); - if (liquidEthAmount < lowWatermarkInETH()) { + function canRedeem(uint256 amount, address token) public view returns (bool) { + uint256 liquidEthAmount = getInstantLiquidityAmount(token); + uint256 lowWatermark = lowWatermarkInETH(token); + if (liquidEthAmount < lowWatermark) { + return false; + } + uint256 availableAmount = liquidEthAmount - lowWatermark; + if (availableAmount < amount) { return false; } uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); - bool consumable = BucketLimiter.canConsume(limit, bucketUnit); + bool consumable = BucketLimiter.canConsume(tokenToRedemptionInfo[token].limit, bucketUnit); return consumable && amount <= liquidEthAmount; } /** * @dev Sets the maximum size of the bucket that can be consumed in a given time period. * @param capacity The capacity of the bucket. + * @param token The token to set the capacity for */ - function setCapacity(uint256 capacity) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { + function setCapacity(uint256 capacity, address token) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { // max capacity = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether, which is practically enough uint64 bucketUnit = _convertToBucketUnit(capacity, Math.Rounding.Down); - BucketLimiter.setCapacity(limit, bucketUnit); + BucketLimiter.setCapacity(tokenToRedemptionInfo[token].limit, bucketUnit); } /** * @dev Sets the rate at which the bucket is refilled per second. * @param refillRate The rate at which the bucket is refilled per second. + * @param token The token to set the refill rate for */ - function setRefillRatePerSecond(uint256 refillRate) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { + function setRefillRatePerSecond(uint256 refillRate, address token) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { // max refillRate = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether per second, which is practically enough uint64 bucketUnit = _convertToBucketUnit(refillRate, Math.Rounding.Down); - BucketLimiter.setRefillRate(limit, bucketUnit); + BucketLimiter.setRefillRate(tokenToRedemptionInfo[token].limit, bucketUnit); } /** * @dev Sets the exit fee. * @param _exitFeeInBps The exit fee. + * @param token The token to set the exit fee for */ - function setExitFeeBasisPoints(uint16 _exitFeeInBps) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { + function setExitFeeBasisPoints(uint16 _exitFeeInBps, address token) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { require(_exitFeeInBps <= BASIS_POINT_SCALE, "INVALID"); - exitFeeInBps = _exitFeeInBps; + tokenToRedemptionInfo[token].exitFeeInBps = _exitFeeInBps; } - function setLowWatermarkInBpsOfTvl(uint16 _lowWatermarkInBpsOfTvl) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { + function setLowWatermarkInBpsOfTvl(uint16 _lowWatermarkInBpsOfTvl, address token) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { require(_lowWatermarkInBpsOfTvl <= BASIS_POINT_SCALE, "INVALID"); - lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; + tokenToRedemptionInfo[token].lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; } - function setExitFeeSplitToTreasuryInBps(uint16 _exitFeeSplitToTreasuryInBps) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { + function setExitFeeSplitToTreasuryInBps(uint16 _exitFeeSplitToTreasuryInBps, address token) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { require(_exitFeeSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); - exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; + tokenToRedemptionInfo[token].exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; } function pauseContract() external hasRole(roleRegistry.PROTOCOL_PAUSER()) { @@ -238,34 +331,34 @@ contract EtherFiRedemptionManager is Initializable, PausableUpgradeable, Reentra _unpause(); } - function _redeemEEth(uint256 eEthAmount, address receiver) internal { + function _redeemEEth(uint256 eEthAmount, address receiver, address outputToken) internal { require(eEthAmount <= eEth.balanceOf(msg.sender), "EtherFiRedemptionManager: Insufficient balance"); - require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); + require(canRedeem(eEthAmount, outputToken), "EtherFiRedemptionManager: Exceeded total redeemable amount"); - (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); + (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount, outputToken); IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), eEthAmount); - _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); + _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury, outputToken); } - function _redeemWeEth(uint256 weEthAmount, address receiver) internal { + function _redeemWeEth(uint256 weEthAmount, address receiver, address outputToken) internal { uint256 eEthAmount = weEth.getEETHByWeETH(weEthAmount); require(weEthAmount <= weEth.balanceOf(msg.sender), "EtherFiRedemptionManager: Insufficient balance"); - require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); + require(canRedeem(eEthAmount, outputToken), "EtherFiRedemptionManager: Exceeded total redeemable amount"); - (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); + (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount, outputToken); IERC20(address(weEth)).safeTransferFrom(msg.sender, address(this), weEthAmount); weEth.unwrap(weEthAmount); - _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); + _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury, outputToken); } - function _updateRateLimit(uint256 amount) internal { + function _updateRateLimit(uint256 amount, address token) internal { uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); - require(BucketLimiter.consume(limit, bucketUnit), "BucketRateLimiter: rate limit exceeded"); + require(BucketLimiter.consume(tokenToRedemptionInfo[token].limit, bucketUnit), "BucketRateLimiter: rate limit exceeded"); } function _convertToBucketUnit(uint256 amount, Math.Rounding rounding) internal pure returns (uint64) { @@ -278,13 +371,13 @@ contract EtherFiRedemptionManager is Initializable, PausableUpgradeable, Reentra } - function _calcRedemption(uint256 ethAmount) internal view returns (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) { + function _calcRedemption(uint256 ethAmount, address token) internal view returns (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) { eEthShares = liquidityPool.sharesForAmount(ethAmount); - eEthAmountToReceiver = liquidityPool.amountForShare(eEthShares.mulDiv(BASIS_POINT_SCALE - exitFeeInBps, BASIS_POINT_SCALE)); // ethShareToReceiver + eEthAmountToReceiver = liquidityPool.amountForShare(eEthShares.mulDiv(BASIS_POINT_SCALE - tokenToRedemptionInfo[token].exitFeeInBps, BASIS_POINT_SCALE)); // ethShareToReceiver sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); uint256 eEthShareFee = eEthShares - sharesToBurn; - feeShareToTreasury = eEthShareFee.mulDiv(exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); + feeShareToTreasury = eEthShareFee.mulDiv(tokenToRedemptionInfo[token].exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); eEthFeeAmountToTreasury = liquidityPool.amountForShare(feeShareToTreasury); } @@ -292,9 +385,9 @@ contract EtherFiRedemptionManager is Initializable, PausableUpgradeable, Reentra * @dev Preview taking an exit fee on redeem. See {IERC4626-previewRedeem}. */ // redeemable amount after exit fee - function previewRedeem(uint256 shares) public view returns (uint256) { + function previewRedeem(uint256 shares, address token) public view returns (uint256) { uint256 amountInEth = liquidityPool.amountForShare(shares); - return amountInEth - _fee(amountInEth, exitFeeInBps); + return amountInEth - _fee(amountInEth, tokenToRedemptionInfo[token].exitFeeInBps); } function _fee(uint256 assets, uint256 feeBasisPoints) internal pure virtual returns (uint256) { diff --git a/src/EtherFiRedemptionManagerTemp.sol b/src/EtherFiRedemptionManagerTemp.sol new file mode 100644 index 00000000..3eac25ab --- /dev/null +++ b/src/EtherFiRedemptionManagerTemp.sol @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; +import "@openzeppelin-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +import "./interfaces/ILiquidityPool.sol"; +import "./interfaces/IeETH.sol"; +import "./interfaces/IWeETH.sol"; + +import "lib/BucketLimiter.sol"; + +import "./RoleRegistry.sol"; + +/* + The contract allows instant redemption of eETH and weETH tokens to ETH with an exit fee. + - It has the exit fee as a percentage of the total amount redeemed. + - It has a rate limiter to limit the total amount that can be redeemed in a given time period. +*/ +contract EtherFiRedemptionManagerTemp is Initializable, PausableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; + using Math for uint256; + + uint256 private constant BUCKET_UNIT_SCALE = 1e12; + uint256 private constant BASIS_POINT_SCALE = 1e4; + + bytes32 public constant ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE = keccak256("ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE"); + + RoleRegistry public immutable roleRegistry; + address public immutable treasury; + IeETH public immutable eEth; + IWeETH public immutable weEth; + ILiquidityPool public immutable liquidityPool; + + BucketLimiter.Limit public limit; + uint16 public exitFeeSplitToTreasuryInBps; + uint16 public exitFeeInBps; + uint16 public lowWatermarkInBpsOfTvl; // bps of TVL + + event Redeemed(address indexed receiver, uint256 redemptionAmount, uint256 feeAmountToTreasury, uint256 feeAmountToStakers); + + receive() external payable {} + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address _liquidityPool, address _eEth, address _weEth, address _treasury, address _roleRegistry) { + roleRegistry = RoleRegistry(_roleRegistry); + treasury = _treasury; + liquidityPool = ILiquidityPool(payable(_liquidityPool)); + eEth = IeETH(_eEth); + weEth = IWeETH(_weEth); + + _disableInitializers(); + } + + function initialize(uint16 _exitFeeSplitToTreasuryInBps, uint16 _exitFeeInBps, uint16 _lowWatermarkInBpsOfTvl, uint256 _bucketCapacity, uint256 _bucketRefillRate) external initializer { + require(_exitFeeInBps <= BASIS_POINT_SCALE, "INVALID"); + require(_exitFeeSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); + require(_lowWatermarkInBpsOfTvl <= BASIS_POINT_SCALE, "INVALID"); + + __UUPSUpgradeable_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + + limit = BucketLimiter.create(_convertToBucketUnit(_bucketCapacity, Math.Rounding.Down), _convertToBucketUnit(_bucketRefillRate, Math.Rounding.Down)); + exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; + exitFeeInBps = _exitFeeInBps; + lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; + } + + function clearOutSlotForUpgrade() external { + require(msg.sender == roleRegistry.owner(), "IncorrectCaller"); + delete limit; + delete exitFeeSplitToTreasuryInBps; + delete exitFeeInBps; + delete lowWatermarkInBpsOfTvl; + } + + /** + * @notice Redeems eETH for ETH. + * @param eEthAmount The amount of eETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + */ + function redeemEEth(uint256 eEthAmount, address receiver) public whenNotPaused nonReentrant { + _redeemEEth(eEthAmount, receiver); + } + + /** + * @notice Redeems weETH for ETH. + * @param weEthAmount The amount of weETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + */ + function redeemWeEth(uint256 weEthAmount, address receiver) public whenNotPaused nonReentrant { + _redeemWeEth(weEthAmount, receiver); + } + + /** + * @notice Redeems eETH for ETH with permit. + * @param eEthAmount The amount of eETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @param permit The permit params. + */ + function redeemEEthWithPermit(uint256 eEthAmount, address receiver, IeETH.PermitInput calldata permit) external whenNotPaused nonReentrant { + try eEth.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} + _redeemEEth(eEthAmount, receiver); + } + + /** + * @notice Redeems weETH for ETH. + * @param weEthAmount The amount of weETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + * @param permit The permit params. + */ + function redeemWeEthWithPermit(uint256 weEthAmount, address receiver, IWeETH.PermitInput calldata permit) external whenNotPaused nonReentrant { + try weEth.permit(msg.sender, address(this), permit.value, permit.deadline, permit.v, permit.r, permit.s) {} catch {} + _redeemWeEth(weEthAmount, receiver); + } + + /** + * @notice Redeems ETH. + * @param ethAmount The amount of ETH to redeem after the exit fee. + * @param receiver The address to receive the redeemed ETH. + */ + function _redeem(uint256 ethAmount, uint256 eEthShares, address receiver, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) internal { + _updateRateLimit(ethAmount); + + // Derive additionals + uint256 eEthShareFee = eEthShares - sharesToBurn; + uint256 feeShareToStakers = eEthShareFee - feeShareToTreasury; + + // Snapshot balances & shares for sanity check at the end + uint256 prevBalance = address(this).balance; + uint256 prevLpBalance = address(liquidityPool).balance; + uint256 totalEEthShare = eEth.totalShares(); + + // Withdraw ETH from the liquidity pool + require(liquidityPool.withdraw(address(this), eEthAmountToReceiver) == sharesToBurn, "invalid num shares burnt"); + uint256 ethReceived = address(this).balance - prevBalance; + + // To Stakers by burning shares + liquidityPool.burnEEthShares(feeShareToStakers); + + // To Treasury by transferring eETH + IERC20(address(eEth)).safeTransfer(treasury, eEthFeeAmountToTreasury); + + // uint256 totalShares = eEth.totalShares(); + require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); + + // To Receiver by transferring ETH, using gas 10k for additional safety + (bool success, ) = receiver.call{value: ethReceived, gas: 10_000}(""); + require(success, "EtherFiRedemptionManager: Transfer failed"); + + // Make sure the liquidity pool balance is correct && total shares are correct + require(address(liquidityPool).balance == prevLpBalance - ethReceived, "EtherFiRedemptionManager: Invalid liquidity pool balance"); + // require(eEth.totalShares() >= 1 gwei && eEth.totalShares() == totalEEthShare - (sharesToBurn + feeShareToStakers), "EtherFiRedemptionManager: Invalid total shares"); + + emit Redeemed(receiver, ethAmount, eEthFeeAmountToTreasury, eEthAmountToReceiver); + } + + /** + * @dev if the contract has less than the low watermark, it will not allow any instant redemption. + */ + function lowWatermarkInETH() public view returns (uint256) { + return liquidityPool.getTotalPooledEther().mulDiv(lowWatermarkInBpsOfTvl, BASIS_POINT_SCALE); + } + + /** + * @dev Returns the total amount that can be redeemed. + */ + function totalRedeemableAmount() external view returns (uint256) { + uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); + if (liquidEthAmount < lowWatermarkInETH()) { + return 0; + } + uint64 consumableBucketUnits = BucketLimiter.consumable(limit); + uint256 consumableAmount = _convertFromBucketUnit(consumableBucketUnits); + return Math.min(consumableAmount, liquidEthAmount); + } + + /** + * @dev Returns whether the given amount can be redeemed. + * @param amount The ETH or eETH amount to check. + */ + function canRedeem(uint256 amount) public view returns (bool) { + uint256 liquidEthAmount = address(liquidityPool).balance - liquidityPool.ethAmountLockedForWithdrawal(); + if (liquidEthAmount < lowWatermarkInETH()) { + return false; + } + uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); + bool consumable = BucketLimiter.canConsume(limit, bucketUnit); + return consumable && amount <= liquidEthAmount; + } + + /** + * @dev Sets the maximum size of the bucket that can be consumed in a given time period. + * @param capacity The capacity of the bucket. + */ + function setCapacity(uint256 capacity) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { + // max capacity = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether, which is practically enough + uint64 bucketUnit = _convertToBucketUnit(capacity, Math.Rounding.Down); + BucketLimiter.setCapacity(limit, bucketUnit); + } + + /** + * @dev Sets the rate at which the bucket is refilled per second. + * @param refillRate The rate at which the bucket is refilled per second. + */ + function setRefillRatePerSecond(uint256 refillRate) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { + // max refillRate = max(uint64) * 1e12 ~= 16 * 1e18 * 1e12 = 16 * 1e12 ether per second, which is practically enough + uint64 bucketUnit = _convertToBucketUnit(refillRate, Math.Rounding.Down); + BucketLimiter.setRefillRate(limit, bucketUnit); + } + + /** + * @dev Sets the exit fee. + * @param _exitFeeInBps The exit fee. + */ + function setExitFeeBasisPoints(uint16 _exitFeeInBps) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { + require(_exitFeeInBps <= BASIS_POINT_SCALE, "INVALID"); + exitFeeInBps = _exitFeeInBps; + } + + function setLowWatermarkInBpsOfTvl(uint16 _lowWatermarkInBpsOfTvl) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { + require(_lowWatermarkInBpsOfTvl <= BASIS_POINT_SCALE, "INVALID"); + lowWatermarkInBpsOfTvl = _lowWatermarkInBpsOfTvl; + } + + function setExitFeeSplitToTreasuryInBps(uint16 _exitFeeSplitToTreasuryInBps) external hasRole(ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE) { + require(_exitFeeSplitToTreasuryInBps <= BASIS_POINT_SCALE, "INVALID"); + exitFeeSplitToTreasuryInBps = _exitFeeSplitToTreasuryInBps; + } + + function pauseContract() external hasRole(roleRegistry.PROTOCOL_PAUSER()) { + _pause(); + } + + function unPauseContract() external hasRole(roleRegistry.PROTOCOL_UNPAUSER()) { + _unpause(); + } + + function _redeemEEth(uint256 eEthAmount, address receiver) internal { + require(eEthAmount <= eEth.balanceOf(msg.sender), "EtherFiRedemptionManager: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); + + (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); + + IERC20(address(eEth)).safeTransferFrom(msg.sender, address(this), eEthAmount); + + _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); + } + + function _redeemWeEth(uint256 weEthAmount, address receiver) internal { + uint256 eEthAmount = weEth.getEETHByWeETH(weEthAmount); + require(weEthAmount <= weEth.balanceOf(msg.sender), "EtherFiRedemptionManager: Insufficient balance"); + require(canRedeem(eEthAmount), "EtherFiRedemptionManager: Exceeded total redeemable amount"); + + (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) = _calcRedemption(eEthAmount); + + IERC20(address(weEth)).safeTransferFrom(msg.sender, address(this), weEthAmount); + weEth.unwrap(weEthAmount); + + _redeem(eEthAmount, eEthShares, receiver, eEthAmountToReceiver, eEthFeeAmountToTreasury, sharesToBurn, feeShareToTreasury); + } + + + function _updateRateLimit(uint256 amount) internal { + uint64 bucketUnit = _convertToBucketUnit(amount, Math.Rounding.Up); + require(BucketLimiter.consume(limit, bucketUnit), "BucketRateLimiter: rate limit exceeded"); + } + + function _convertToBucketUnit(uint256 amount, Math.Rounding rounding) internal pure returns (uint64) { + require(amount < type(uint64).max * BUCKET_UNIT_SCALE, "EtherFiRedemptionManager: Amount too large"); + return (rounding == Math.Rounding.Up) ? SafeCast.toUint64((amount + BUCKET_UNIT_SCALE - 1) / BUCKET_UNIT_SCALE) : SafeCast.toUint64(amount / BUCKET_UNIT_SCALE); + } + + function _convertFromBucketUnit(uint64 bucketUnit) internal pure returns (uint256) { + return bucketUnit * BUCKET_UNIT_SCALE; + } + + + function _calcRedemption(uint256 ethAmount) internal view returns (uint256 eEthShares, uint256 eEthAmountToReceiver, uint256 eEthFeeAmountToTreasury, uint256 sharesToBurn, uint256 feeShareToTreasury) { + eEthShares = liquidityPool.sharesForAmount(ethAmount); + eEthAmountToReceiver = liquidityPool.amountForShare(eEthShares.mulDiv(BASIS_POINT_SCALE - exitFeeInBps, BASIS_POINT_SCALE)); // ethShareToReceiver + + sharesToBurn = liquidityPool.sharesForWithdrawalAmount(eEthAmountToReceiver); + uint256 eEthShareFee = eEthShares - sharesToBurn; + feeShareToTreasury = eEthShareFee.mulDiv(exitFeeSplitToTreasuryInBps, BASIS_POINT_SCALE); + eEthFeeAmountToTreasury = liquidityPool.amountForShare(feeShareToTreasury); + } + + /** + * @dev Preview taking an exit fee on redeem. See {IERC4626-previewRedeem}. + */ + // redeemable amount after exit fee + function previewRedeem(uint256 shares) public view returns (uint256) { + uint256 amountInEth = liquidityPool.amountForShare(shares); + return amountInEth - _fee(amountInEth, exitFeeInBps); + } + + function _fee(uint256 assets, uint256 feeBasisPoints) internal pure virtual returns (uint256) { + return assets.mulDiv(feeBasisPoints, BASIS_POINT_SCALE, Math.Rounding.Up); + } + + function _authorizeUpgrade(address newImplementation) internal override { + roleRegistry.onlyProtocolUpgrader(msg.sender); + } + + function getImplementation() external view returns (address) { + return _getImplementation(); + } + + function _hasRole(bytes32 role, address account) internal view returns (bool) { + require(roleRegistry.hasRole(role, account), "EtherFiRedemptionManager: Unauthorized"); + } + + modifier hasRole(bytes32 role) { + _hasRole(role, msg.sender); + _; + } + +} \ No newline at end of file diff --git a/src/EtherFiRestaker.sol b/src/EtherFiRestaker.sol index 71cb06eb..f3393f62 100644 --- a/src/EtherFiRestaker.sol +++ b/src/EtherFiRestaker.sol @@ -28,6 +28,7 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, } IRewardsCoordinator public immutable rewardsCoordinator; + address public immutable etherFiRedemptionManager; LiquidityPool public liquidityPool; Liquifier public liquifier; @@ -59,8 +60,9 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, error IncorrectCaller(); /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address _rewardsCoordinator) { + constructor(address _rewardsCoordinator, address _etherFiRedemptionManager) { rewardsCoordinator = IRewardsCoordinator(_rewardsCoordinator); + etherFiRedemptionManager = _etherFiRedemptionManager; _disableInitializers(); } @@ -92,6 +94,15 @@ contract EtherFiRestaker is Initializable, UUPSUpgradeable, OwnableUpgradeable, // | Handling Lido's stETH | // |--------------------------------------------------------------------------------------------| + /// @notice Transfer stETH to a recipient for instant withdrawal + /// @param recipient The address to receive stETH + /// @param amount The amount of stETH to transfer + function transferStETH(address recipient, uint256 amount) external { + if(msg.sender != etherFiRedemptionManager) revert IncorrectCaller(); + require(amount <= lido.balanceOf(address(this)), "EtherFiRestaker: Insufficient stETH balance"); + IERC20(address(lido)).safeTransfer(recipient, amount); + } + /// Initiate the redemption of stETH for ETH /// @notice Request for all stETH holdings function stEthRequestWithdrawal() external onlyAdmin returns (uint256[] memory) { diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 85c01df3..98380da3 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -84,6 +84,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL event Deposit(address indexed sender, uint256 amount, SourceOfFunds source, address referral); event Withdraw(address indexed sender, address recipient, uint256 amount, SourceOfFunds source); + event EEthSharesBurnedForNonETHWithdrawal(uint256 amountSharesToBurn, uint256 withdrawalValueInETH); event UpdatedWhitelist(address userAddress, bool value); event UpdatedTreasury(address newTreasury); event UpdatedFeeRecipient(address newFeeRecipient); @@ -467,6 +468,20 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL eETH.burnShares(msg.sender, shares); } + function burnEEthSharesForNonETHWithdrawal(uint256 _amountSharesToBurn, uint256 _withdrawalValueInETH) external { + uint256 share = sharesForWithdrawalAmount(_withdrawalValueInETH); + if (msg.sender != address(etherFiRedemptionManager)) revert IncorrectCaller(); + if (_amountSharesToBurn == 0 || _withdrawalValueInETH == 0) revert InvalidAmount(); + + // Verify the share price will not go down + if (share > _amountSharesToBurn) revert InvalidAmount(); + + totalValueOutOfLp -= uint128(_withdrawalValueInETH); + + eETH.burnShares(msg.sender, _amountSharesToBurn); + emit EEthSharesBurnedForNonETHWithdrawal(_amountSharesToBurn, _withdrawalValueInETH); + } + //-------------------------------------------------------------------------------------- //------------------------------ INTERNAL FUNCTIONS ---------------------------------- //-------------------------------------------------------------------------------------- diff --git a/src/interfaces/ILiquidityPool.sol b/src/interfaces/ILiquidityPool.sol index 2400d7fb..23374139 100644 --- a/src/interfaces/ILiquidityPool.sol +++ b/src/interfaces/ILiquidityPool.sol @@ -57,6 +57,7 @@ interface ILiquidityPool { function deposit(address _user, address _referral) external payable returns (uint256); function depositToRecipient(address _recipient, uint256 _amount, address _referral) external returns (uint256); function withdraw(address _recipient, uint256 _amount) external returns (uint256); + function burnEEthSharesForNonETHWithdrawal(uint256 _amountSharesToBurn, uint256 _withdrawalValueInETH) external; function requestWithdraw(address recipient, uint256 amount) external returns (uint256); function requestWithdrawWithPermit(address _owner, uint256 _amount, PermitInput calldata _permit) external returns (uint256); function requestMembershipNFTWithdraw(address recipient, uint256 amount, uint256 fee) external returns (uint256); diff --git a/test/ContractCodeChecker.t.sol b/test/ContractCodeChecker.t.sol index 58c59ad9..2f4976c3 100644 --- a/test/ContractCodeChecker.t.sol +++ b/test/ContractCodeChecker.t.sol @@ -24,7 +24,7 @@ contract ContractCodeCheckerTest is TestSetup { EtherFiNode etherFiNodeImplementation = new EtherFiNode(address(0x0), address(0x0), address(0x0), address(0x0), address(0x0)); address etherFiNodeImplAddress = address(0xc5F2764383f93259Fba1D820b894B1DE0d47937e); - EtherFiRestaker etherFiRestakerImplementation = new EtherFiRestaker(address(0x7750d328b314EfFa365A0402CcfD489B80B0adda)); + EtherFiRestaker etherFiRestakerImplementation = new EtherFiRestaker(address(0x7750d328b314EfFa365A0402CcfD489B80B0adda), address(0x0)); address etherFiRestakerImplAddress = address(0x0052F731a6BEA541843385ffBA408F52B74Cb624); // Verify bytecode matches between deployed contracts and their implementations diff --git a/test/EtherFiRedemptionManager.t.sol b/test/EtherFiRedemptionManager.t.sol index 6188717c..5744cb49 100644 --- a/test/EtherFiRedemptionManager.t.sol +++ b/test/EtherFiRedemptionManager.t.sol @@ -3,12 +3,16 @@ pragma solidity ^0.8.13; import "forge-std/console2.sol"; import "./TestSetup.sol"; +import "lib/BucketLimiter.sol"; contract EtherFiRedemptionManagerTest is TestSetup { + address public constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address user = vm.addr(999); address op_admin = vm.addr(1000); + function setUp() public { setUpTests(); } @@ -16,11 +20,11 @@ contract EtherFiRedemptionManagerTest is TestSetup { function setUp_Fork() public { setUpTests(); initializeRealisticFork(MAINNET_FORK); - vm.startPrank(roleRegistryInstance.owner()); roleRegistryInstance.grantRole(keccak256("ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE"), op_admin); + LiquidityPool liquidityPoolImpl = new LiquidityPool(); + liquidityPoolInstance.upgradeTo(payable(address(liquidityPoolImpl))); vm.stopPrank(); - } function test_upgrade_only_by_owner() public { @@ -31,69 +35,74 @@ contract EtherFiRedemptionManagerTest is TestSetup { vm.expectRevert(); etherFiRedemptionManagerInstance.upgradeTo(impl); - vm.prank(admin); + vm.prank(owner); etherFiRedemptionManagerInstance.upgradeTo(impl); } function test_rate_limit() public { - vm.deal(user, 1000 ether); + + + vm.deal(user, 100000 ether); vm.prank(user); - liquidityPoolInstance.deposit{value: 1000 ether}(); + liquidityPoolInstance.deposit{value: 100000 ether}(); - assertEq(etherFiRedemptionManagerInstance.canRedeem(1 ether), true); - assertEq(etherFiRedemptionManagerInstance.canRedeem(5 ether - 1), true); - assertEq(etherFiRedemptionManagerInstance.canRedeem(5 ether + 1), false); - assertEq(etherFiRedemptionManagerInstance.canRedeem(10 ether), false); - assertEq(etherFiRedemptionManagerInstance.totalRedeemableAmount(), 5 ether); + vm.deal(user, 5 ether); + vm.prank(user); + liquidityPoolInstance.deposit{value: 5 ether}(); + + assertEq(etherFiRedemptionManagerInstance.canRedeem(1 ether, ETH_ADDRESS), true); + assertEq(etherFiRedemptionManagerInstance.canRedeem(5 ether - 1, ETH_ADDRESS), true); + assertEq(etherFiRedemptionManagerInstance.canRedeem(5 ether + 1, ETH_ADDRESS), false); + assertEq(etherFiRedemptionManagerInstance.canRedeem(10 ether, ETH_ADDRESS), false); + assertEq(etherFiRedemptionManagerInstance.totalRedeemableAmount(ETH_ADDRESS), 5 ether); } function test_lowwatermark_guardrail() public { vm.deal(user, 100 ether); - assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(), 0 ether); + assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(ETH_ADDRESS), 0 ether); vm.prank(user); liquidityPoolInstance.deposit{value: 100 ether}(); + vm.startPrank(owner); - etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(1_00); // 1% - assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(), 1 ether); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(1_00, ETH_ADDRESS); // 1% + assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(ETH_ADDRESS), 1 ether); - etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(50_00); // 50% - assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(), 50 ether); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(50_00, ETH_ADDRESS); // 50% + assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(ETH_ADDRESS), 50 ether); - etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(100_00); // 100% - assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(), 100 ether); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(100_00, ETH_ADDRESS); // 100% + assertEq(etherFiRedemptionManagerInstance.lowWatermarkInETH(ETH_ADDRESS), 100 ether); vm.expectRevert("INVALID"); - etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(100_01); // 100.01% + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(100_01, ETH_ADDRESS); // 100.01% } - function test_admin_permission() public { + function _admin_permission_by_token(address token) public { vm.startPrank(alice); vm.expectRevert(); - etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(1_00); // 1% + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(1_00, token); // 1% vm.expectRevert(); - etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(1_000); // 10% + etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(1_000, token); // 10% vm.expectRevert(); - etherFiRedemptionManagerInstance.setExitFeeBasisPoints(40); // 0.4% + etherFiRedemptionManagerInstance.setExitFeeBasisPoints(40, token); // 0.4% vm.expectRevert(); - etherFiRedemptionManagerInstance.setRefillRatePerSecond(1_00); // 1% + etherFiRedemptionManagerInstance.setRefillRatePerSecond(1_00, token); // 1% vm.expectRevert(); - etherFiRedemptionManagerInstance.setCapacity(1_00); // 1% + etherFiRedemptionManagerInstance.setCapacity(1_00, token); // 1% vm.expectRevert(); - etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(1_00); // 1% + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(1_00, token); // 1% vm.stopPrank(); } - function testFuzz_redeemEEth( - uint256 depositAmount, - uint256 redeemAmount, - uint256 exitFeeSplitBps, - uint16 exitFeeBps, - uint16 lowWatermarkBps - ) public { - + function test_admin_permission_by_token() public { + _admin_permission_by_token(ETH_ADDRESS); + _admin_permission_by_token(address(etherFiRestakerInstance.lido())); + } + + function testFuzz_redeemEEth(uint256 depositAmount,uint256 redeemAmount,uint256 exitFeeSplitBps,uint16 exitFeeBps,uint16 lowWatermarkBps) public { vm.assume(depositAmount >= redeemAmount); depositAmount = bound(depositAmount, 1 ether, 1000 ether); redeemAmount = bound(redeemAmount, 0.1 ether, depositAmount); @@ -109,22 +118,22 @@ contract EtherFiRedemptionManagerTest is TestSetup { // Set exitFeeSplitToTreasuryInBps vm.prank(owner); - etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); + etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps), ETH_ADDRESS); // Set exitFeeBasisPoints and lowWatermarkInBpsOfTvl vm.prank(owner); - etherFiRedemptionManagerInstance.setExitFeeBasisPoints(exitFeeBps); + etherFiRedemptionManagerInstance.setExitFeeBasisPoints(exitFeeBps, ETH_ADDRESS); vm.prank(owner); - etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps, ETH_ADDRESS); vm.startPrank(user); - if (etherFiRedemptionManagerInstance.canRedeem(redeemAmount)) { + if (etherFiRedemptionManagerInstance.canRedeem(redeemAmount, ETH_ADDRESS)) { uint256 userBalanceBefore = address(user).balance; uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); IeETH.PermitInput memory permit = eEth_createPermitInput(999, address(etherFiRedemptionManagerInstance), redeemAmount, eETHInstance.nonces(user), 2**256 - 1, eETHInstance.DOMAIN_SEPARATOR()); - etherFiRedemptionManagerInstance.redeemEEthWithPermit(redeemAmount, user, permit); + etherFiRedemptionManagerInstance.redeemEEthWithPermit(redeemAmount, user, permit, ETH_ADDRESS); uint256 totalFee = (redeemAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; @@ -143,22 +152,14 @@ contract EtherFiRedemptionManagerTest is TestSetup { } else { vm.expectRevert(); - etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, ETH_ADDRESS); } vm.stopPrank(); } - function testFuzz_redeemWeEth( - uint256 depositAmount, - uint256 redeemAmount, - uint16 exitFeeSplitBps, - int256 rebase, - uint16 exitFeeBps, - uint16 lowWatermarkBps - ) public { + function testFuzz_redeemWeEth(uint256 depositAmount,uint256 redeemAmount,uint16 exitFeeSplitBps,int256 rebase,uint16 exitFeeBps,uint16 lowWatermarkBps) public { // Bound the parameters depositAmount = bound(depositAmount, 1 ether, 1000 ether); - console2.log(depositAmount); redeemAmount = bound(redeemAmount, 0.1 ether, depositAmount); exitFeeSplitBps = uint16(bound(exitFeeSplitBps, 0, 10000)); exitFeeBps = uint16(bound(exitFeeBps, 0, 10000)); @@ -177,13 +178,13 @@ contract EtherFiRedemptionManagerTest is TestSetup { // Set fee and watermark configurations vm.prank(owner); - etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps)); + etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(uint16(exitFeeSplitBps), ETH_ADDRESS); vm.prank(owner); - etherFiRedemptionManagerInstance.setExitFeeBasisPoints(exitFeeBps); + etherFiRedemptionManagerInstance.setExitFeeBasisPoints(exitFeeBps, ETH_ADDRESS); vm.prank(owner); - etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(lowWatermarkBps, ETH_ADDRESS); // Convert redeemAmount from ETH to weETH vm.startPrank(user); @@ -191,14 +192,14 @@ contract EtherFiRedemptionManagerTest is TestSetup { weEthInstance.wrap(redeemAmount); uint256 weEthAmount = weEthInstance.balanceOf(user); - if (etherFiRedemptionManagerInstance.canRedeem(redeemAmount)) { + if (etherFiRedemptionManagerInstance.canRedeem(redeemAmount, ETH_ADDRESS)) { uint256 userBalanceBefore = address(user).balance; uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(treasuryInstance)); uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); IWeETH.PermitInput memory permit = weEth_createPermitInput(999, address(etherFiRedemptionManagerInstance), weEthAmount, weEthInstance.nonces(user), 2**256 - 1, weEthInstance.DOMAIN_SEPARATOR()); - etherFiRedemptionManagerInstance.redeemWeEthWithPermit(weEthAmount, user, permit); + etherFiRedemptionManagerInstance.redeemWeEthWithPermit(weEthAmount, user, permit, ETH_ADDRESS); uint256 totalFee = (eEthAmount * exitFeeBps) / 10000; uint256 treasuryFee = (totalFee * exitFeeSplitBps) / 10000; @@ -217,7 +218,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { } else { vm.expectRevert(); - etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, ETH_ADDRESS); } vm.stopPrank(); } @@ -243,11 +244,11 @@ contract EtherFiRedemptionManagerTest is TestSetup { // Admin performs admin-only actions vm.startPrank(admin); - etherFiRedemptionManagerInstance.setCapacity(10 ether); - etherFiRedemptionManagerInstance.setRefillRatePerSecond(0.001 ether); - etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(1e4); - etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(1e2); - etherFiRedemptionManagerInstance.setExitFeeBasisPoints(1e2); + etherFiRedemptionManagerInstance.setCapacity(10 ether, ETH_ADDRESS); + etherFiRedemptionManagerInstance.setRefillRatePerSecond(0.001 ether, ETH_ADDRESS); + etherFiRedemptionManagerInstance.setExitFeeSplitToTreasuryInBps(1e4, ETH_ADDRESS); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(1e2, ETH_ADDRESS); + etherFiRedemptionManagerInstance.setExitFeeBasisPoints(1e2, ETH_ADDRESS); vm.stopPrank(); // Pauser pauses the contract @@ -269,7 +270,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { // Admin attempts admin-only actions after role revocation vm.startPrank(admin); vm.expectRevert("EtherFiRedemptionManager: Unauthorized"); - etherFiRedemptionManagerInstance.setCapacity(10 ether); + etherFiRedemptionManagerInstance.setCapacity(10 ether, ETH_ADDRESS); vm.stopPrank(); // Pauser attempts to unpause (should fail) @@ -300,33 +301,33 @@ contract EtherFiRedemptionManagerTest is TestSetup { vm.prank(alice); liquidityPoolInstance.deposit{value: 100000 ether}(); - vm.deal(user, 100 ether); + vm.deal(user, 2010 ether); vm.startPrank(user); - liquidityPoolInstance.deposit{value: 10 ether}(); + liquidityPoolInstance.deposit{value: 2005 ether}(); - uint256 redeemableAmount = etherFiRedemptionManagerInstance.totalRedeemableAmount(); + uint256 redeemableAmount = etherFiRedemptionManagerInstance.totalRedeemableAmount(ETH_ADDRESS); uint256 userBalance = address(user).balance; uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); - eETHInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); - etherFiRedemptionManagerInstance.redeemEEth(1 ether, user); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), 2000 ether); + etherFiRedemptionManagerInstance.redeemEEth(2000 ether, user, ETH_ADDRESS); - uint256 totalFee = (1 ether * 1e2) / 1e4; + uint256 totalFee = (2000 ether * 1e2) / 1e4; uint256 treasuryFee = (totalFee * 1e3) / 1e4; - uint256 userReceives = 1 ether - totalFee; + uint256 userReceives = 2000 ether - totalFee; assertApproxEqAbs(eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())), treasuryBalance + treasuryFee, 1e1); assertApproxEqAbs(address(user).balance, userBalance + userReceives, 1e1); - eETHInstance.approve(address(etherFiRedemptionManagerInstance), 5 ether); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); vm.expectRevert("EtherFiRedemptionManager: Exceeded total redeemable amount"); - etherFiRedemptionManagerInstance.redeemEEth(5 ether, user); + etherFiRedemptionManagerInstance.redeemEEth(1 ether, user, ETH_ADDRESS); vm.stopPrank(); } - function test_mainnet_redeem_weEth_with_rebase() public { + function test_mainnet_redeem_weEth_for_stETH_with_rebase() public { setUp_Fork(); vm.deal(alice, 50000 ether); @@ -352,7 +353,7 @@ contract EtherFiRedemptionManagerTest is TestSetup { uint256 userBalance = address(user).balance; uint256 treasuryBalance = eETHInstance.balanceOf(address(treasuryInstance)); weEthInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); - etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, ETH_ADDRESS); uint256 totalFee = (eEthAmount * 1e2) / 1e4; uint256 treasuryFee = (totalFee * 1e3) / 1e4; @@ -372,8 +373,8 @@ contract EtherFiRedemptionManagerTest is TestSetup { eETHInstance.mintShares(user, 2 * redeemAmount); vm.startPrank(op_admin); - etherFiRedemptionManagerInstance.setCapacity(2 * redeemAmount); - etherFiRedemptionManagerInstance.setRefillRatePerSecond(2 * redeemAmount); + etherFiRedemptionManagerInstance.setCapacity(2 * redeemAmount, ETH_ADDRESS); + etherFiRedemptionManagerInstance.setRefillRatePerSecond(2 * redeemAmount, ETH_ADDRESS); vm.stopPrank(); vm.warp(block.timestamp + 1); @@ -385,8 +386,237 @@ contract EtherFiRedemptionManagerTest is TestSetup { eETHInstance.approve(address(etherFiRedemptionManagerInstance), redeemAmount); vm.expectRevert("EtherFiRedemptionManager: Exceeded total redeemable amount"); - etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user); + etherFiRedemptionManagerInstance.redeemEEth(redeemAmount, user, ETH_ADDRESS); + + vm.stopPrank(); + } + + function test_mainnet_redeem_eEth_for_stETH() public { + setUp_Fork(); + ILido stEth = ILido(address(etherFiRestakerInstance.lido())); + vm.deal(user, 2010 ether); + vm.startPrank(user); + liquidityPoolInstance.deposit{value: 2005 ether}(); + + uint256 redeemableAmount = etherFiRedemptionManagerInstance.totalRedeemableAmount(address(etherFiRestakerInstance.lido())); + console2.log("redeemableAmount", redeemableAmount); + uint256 userBalance = address(user).balance; + uint256 treasuryBalance = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); + + eETHInstance.approve(address(etherFiRedemptionManagerInstance), 2000 ether); + etherFiRedemptionManagerInstance.redeemEEth(2000 ether, user, address(etherFiRestakerInstance.lido())); + + redeemableAmount = etherFiRedemptionManagerInstance.totalRedeemableAmount(address(etherFiRestakerInstance.lido())); + console2.log("redeemableAmount", redeemableAmount); + + uint256 totalFee = (2000 ether * 1e2) / 1e4; + uint256 treasuryFee = (totalFee * 1e3) / 1e4; + uint256 userReceives = 2000 ether - totalFee; + assertApproxEqAbs(eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())), treasuryBalance + treasuryFee, 1e1); + assertApproxEqAbs(stEth.balanceOf(user), userReceives, 1e1); + + eETHInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); + address lidoToken = address(etherFiRestakerInstance.lido()); // external call; fetch before expectRevert + vm.expectRevert("EtherFiRedemptionManager: Exceeded total redeemable amount"); + etherFiRedemptionManagerInstance.redeemEEth(1 ether, user, lidoToken); + + vm.stopPrank(); + } + + function test_mainnet_redeem_weEth_with_rebase() public { + setUp_Fork(); + + vm.deal(alice, 50000 ether); + vm.prank(alice); + liquidityPoolInstance.deposit{value: 50000 ether}(); + + vm.deal(user, 100 ether); + + vm.startPrank(user); + liquidityPoolInstance.deposit{value: 10 ether}(); + eETHInstance.approve(address(weEthInstance), 10 ether); + weEthInstance.wrap(1 ether); + vm.stopPrank(); + + uint256 one_percent_of_tvl = liquidityPoolInstance.getTotalPooledEther() / 100; + + vm.prank(address(membershipManagerV1Instance)); + liquidityPoolInstance.rebase(int128(uint128(one_percent_of_tvl))); // 10 eETH earned 1 ETH + vm.startPrank(user); + uint256 weEthAmount = weEthInstance.balanceOf(user); + uint256 eEthAmount = liquidityPoolInstance.amountForShare(weEthAmount); + uint256 userBalance = address(user).balance; + uint256 treasuryBalance = eETHInstance.balanceOf(address(treasuryInstance)); + weEthInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); + etherFiRedemptionManagerInstance.redeemWeEth(weEthAmount, user, address(etherFiRestakerInstance.lido())); + + uint256 totalFee = (eEthAmount * 1e2) / 1e4; + uint256 treasuryFee = (totalFee * 1e3) / 1e4; + uint256 userReceives = eEthAmount - totalFee; + + assertApproxEqAbs(eETHInstance.balanceOf(address(treasuryInstance)), treasuryBalance + treasuryFee, 1e1); + assertApproxEqAbs(stEth.balanceOf(user), userReceives, 1e1); + + vm.stopPrank(); + } + + function test_unrestaker_transferSteth_permissions() public { + setUp_Fork(); + + vm.expectRevert(EtherFiRestaker.IncorrectCaller.selector); + vm.startPrank(admin); + etherFiRestakerInstance.transferStETH(user, 1 ether); + vm.stopPrank(); + + vm.expectRevert(EtherFiRestaker.IncorrectCaller.selector); + vm.startPrank(owner); + etherFiRestakerInstance.transferStETH(user, 1 ether); + vm.stopPrank(); + + uint256 balanceBefore = etherFiRestakerInstance.lido().balanceOf(user); + vm.prank(address(etherFiRedemptionManagerInstance)); + etherFiRestakerInstance.transferStETH(user, 1 ether); + uint256 balanceAfter = etherFiRestakerInstance.lido().balanceOf(user); + assertApproxEqAbs(balanceAfter, balanceBefore + 1 ether, 2); + } + + function test_end_to_end_redeem_stETH() public { + setUp_Fork(); + vm.startPrank(op_admin); + address[] memory _tokens = new address[](1); + _tokens[0] = address(etherFiRestakerInstance.lido()); + uint16[] memory _exitFeeSplitToTreasuryInBps = new uint16[](1); + _exitFeeSplitToTreasuryInBps[0] = 20_00; + uint16[] memory _exitFeeInBps = new uint16[](1); + _exitFeeInBps[0] = 3_00; + uint16[] memory _lowWatermarkInBpsOfTvl = new uint16[](1); + _lowWatermarkInBpsOfTvl[0] = 2_00; + uint256[] memory _bucketCapacity = new uint256[](1); + _bucketCapacity[0] = 10 ether; + uint256[] memory _bucketRefillRate = new uint256[](1); + _bucketRefillRate[0] = 0.001 ether; + etherFiRedemptionManagerInstance.initializeTokenParameters(_tokens, _exitFeeSplitToTreasuryInBps, _exitFeeInBps, _lowWatermarkInBpsOfTvl, _bucketCapacity, _bucketRefillRate); + + // verify the struct has the correct values for stETH after update token parameters + (BucketLimiter.Limit memory limit, uint16 exitSplit, uint16 exitFee, uint16 lowWM) = + etherFiRedemptionManagerInstance.tokenToRedemptionInfo(address(etherFiRestakerInstance.lido())); + assertEq(exitSplit, 20_00); + assertEq(exitFee, 3_00); + assertEq(lowWM, 2_00); + uint64 expectedCapacity = uint64(10 ether / 1e12); + uint64 expectedRefillRate = uint64(0.001 ether / 1e12); + assertEq(limit.capacity, expectedCapacity); + assertEq(limit.refillRate, expectedRefillRate); + vm.stopPrank(); + + //test low watermark works + vm.startPrank(user); + vm.deal(user, 10 ether); + liquidityPoolInstance.deposit{value: 10 ether}(); + address lidoToken = address(etherFiRestakerInstance.lido()); // external call; fetch before expectRevert + vm.expectRevert(bytes("EtherFiRedemptionManager: Exceeded total redeemable amount")); + etherFiRedemptionManagerInstance.redeemEEth(1 ether, user, lidoToken); + vm.stopPrank(); + vm.startPrank(op_admin); + etherFiRedemptionManagerInstance.setLowWatermarkInBpsOfTvl(0, address(etherFiRestakerInstance.lido())); + vm.stopPrank(); + vm.startPrank(user); + uint256 balanceBefore = stEth.balanceOf(user); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), 1 ether); + etherFiRedemptionManagerInstance.redeemEEth(1 ether, user, address(etherFiRestakerInstance.lido())); + uint256 balanceAfter = stEth.balanceOf(user); + assertApproxEqAbs(balanceAfter, balanceBefore + 1 ether - 0.03 ether, 1e1); + vm.stopPrank(); + } + + function test_redeem_stETH_share_price() public { + setUp_Fork(); + vm.startPrank(user); + vm.deal(user, 10 ether); + liquidityPoolInstance.deposit{value: 10 ether}(); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), 10 ether); + //get number of shares for 1 ether + uint256 sharesFor_999_ether = liquidityPoolInstance.sharesForAmount(0.999 ether); // should be 0.9 ether since 0.1 ether is left for + address lidoToken = address(etherFiRestakerInstance.lido()); // external call; fetch before expectRevert + uint256 totalValueOutOfLpBefore = liquidityPoolInstance.totalValueOutOfLp(); + uint256 totalValueInLpBefore = liquidityPoolInstance.totalValueInLp(); + uint256 totalSharesBefore = eETHInstance.totalShares(); + uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); + //steth balance before + uint256 stethBalanceBefore = stEth.balanceOf(user); + etherFiRedemptionManagerInstance.redeemEEth(1 ether, user, lidoToken); + uint256 stethBalanceAfter = stEth.balanceOf(user); + uint256 totalSharesAfter = eETHInstance.totalShares(); + uint256 totalValueOutOfLpAfter = liquidityPoolInstance.totalValueOutOfLp(); + uint256 totalValueInLpAfter = liquidityPoolInstance.totalValueInLp(); + uint256 treasuryBalanceAfter = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); + assertApproxEqAbs(stethBalanceAfter - stethBalanceBefore, 0.99 ether, 3); + assertApproxEqAbs(totalSharesBefore - totalSharesAfter, sharesFor_999_ether, 1); + assertApproxEqAbs(totalValueOutOfLpBefore - totalValueOutOfLpAfter, 0.99 ether, 3); + assertEq(totalValueInLpAfter- totalValueInLpBefore, 0); + assertApproxEqAbs(treasuryBalanceAfter-treasuryBalanceBefore, 0.001 ether, 3); + vm.stopPrank(); + } + + function test_redeem_stETH_share_price_with_not_fee() public { + setUp_Fork(); + vm.startPrank(user); + vm.deal(user, 10 ether); + liquidityPoolInstance.deposit{value: 10 ether}(); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), 10 ether); + vm.stopPrank(); + //set fee to 0 + vm.startPrank(op_admin); + etherFiRedemptionManagerInstance.setExitFeeBasisPoints(0, address(etherFiRestakerInstance.lido())); + vm.stopPrank(); + //get number of shares for 1 ether + vm.startPrank(user); + uint256 sharesFor_999_ether = liquidityPoolInstance.sharesForAmount(1 ether); // should be 0.9 ether since 0.1 ether is left for + address lidoToken = address(etherFiRestakerInstance.lido()); // external call; fetch before expectRevert + uint256 totalValueOutOfLpBefore = liquidityPoolInstance.totalValueOutOfLp(); + uint256 totalValueInLpBefore = liquidityPoolInstance.totalValueInLp(); + uint256 totalSharesBefore = eETHInstance.totalShares(); + uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); + //steth balance before + uint256 stethBalanceBefore = stEth.balanceOf(user); + etherFiRedemptionManagerInstance.redeemEEth(1 ether, user, lidoToken); + uint256 stethBalanceAfter = stEth.balanceOf(user); + uint256 totalSharesAfter = eETHInstance.totalShares(); + uint256 totalValueOutOfLpAfter = liquidityPoolInstance.totalValueOutOfLp(); + uint256 totalValueInLpAfter = liquidityPoolInstance.totalValueInLp(); + uint256 treasuryBalanceAfter = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); + assertApproxEqAbs(stethBalanceAfter - stethBalanceBefore, 1 ether, 3); + assertApproxEqAbs(totalSharesBefore - totalSharesAfter, sharesFor_999_ether, 1); + assertApproxEqAbs(totalValueOutOfLpBefore - totalValueOutOfLpAfter, 1 ether, 3); + assertEq(totalValueInLpAfter- totalValueInLpBefore, 0); + assertEq(treasuryBalanceAfter-treasuryBalanceBefore, 0); + vm.stopPrank(); + } + + function test_redeem_eEth_share_price() public { + setUp_Fork(); + vm.startPrank(user); + vm.deal(user, 10 ether); + liquidityPoolInstance.deposit{value: 10 ether}(); + eETHInstance.approve(address(etherFiRedemptionManagerInstance), 10 ether); + uint256 sharesFor_999_ether = liquidityPoolInstance.sharesForAmount(0.999 ether); // should be 0.9 ether since 0.1 ether is left for + uint256 totalValueOutOfLpBefore = liquidityPoolInstance.totalValueOutOfLp(); + uint256 totalValueInLpBefore = liquidityPoolInstance.totalValueInLp(); + uint256 totalSharesBefore = eETHInstance.totalShares(); + uint256 treasuryBalanceBefore = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); + uint256 ethBalanceBefore = address(user).balance; + etherFiRedemptionManagerInstance.redeemEEth(1 ether, user, ETH_ADDRESS); + uint256 ethBalanceAfter = address(user).balance; + uint256 totalSharesAfter = eETHInstance.totalShares(); + uint256 totalValueOutOfLpAfter = liquidityPoolInstance.totalValueOutOfLp(); + uint256 totalValueInLpAfter = liquidityPoolInstance.totalValueInLp(); + uint256 treasuryBalanceAfter = eETHInstance.balanceOf(address(etherFiRedemptionManagerInstance.treasury())); + assertApproxEqAbs(ethBalanceAfter - ethBalanceBefore, 0.99 ether, 2); + assertApproxEqAbs(totalSharesBefore - totalSharesAfter, sharesFor_999_ether, 1); + assertApproxEqAbs(totalValueInLpBefore - totalValueInLpAfter, 0.99 ether, 2); + assertEq(totalValueOutOfLpAfter- totalValueOutOfLpBefore, 0); + assertApproxEqAbs(treasuryBalanceAfter-treasuryBalanceBefore, 0.001 ether, 2); vm.stopPrank(); } } diff --git a/test/EtherFiRestaker.t.sol b/test/EtherFiRestaker.t.sol index 7ddc1f2e..83bb434b 100644 --- a/test/EtherFiRestaker.t.sol +++ b/test/EtherFiRestaker.t.sol @@ -268,7 +268,7 @@ contract EtherFiRestakerTest is TestSetup { EtherFiRestaker restaker = EtherFiRestaker(payable(0x1B7a4C3797236A1C37f8741c0Be35c2c72736fFf)); address _claimer = vm.addr(433); - address newRestakerImpl = address(new EtherFiRestaker(address(eigenLayerRewardsCoordinator))); + address newRestakerImpl = address(new EtherFiRestaker(address(eigenLayerRewardsCoordinator), address(etherFiRedemptionManagerInstance))); vm.startPrank(restaker.owner()); restaker.upgradeTo(newRestakerImpl); diff --git a/test/TestSetup.sol b/test/TestSetup.sol index 312f9513..773b867c 100644 --- a/test/TestSetup.sol +++ b/test/TestSetup.sol @@ -49,6 +49,7 @@ import "../src/EtherFiTimelock.sol"; import "../src/BucketRateLimiter.sol"; import "../src/EtherFiRedemptionManager.sol"; +import "../src/EtherFiRedemptionManagerTemp.sol"; import "../script/ContractCodeChecker.sol"; import "../script/Create2Factory.sol"; @@ -408,7 +409,50 @@ contract TestSetup is Test, ContractCodeChecker, DepositDataGeneration { etherFiTimelockInstance = EtherFiTimelock(payable(addressProviderInstance.getContractAddress("EtherFiTimelock"))); etherFiAdminInstance = EtherFiAdmin(payable(addressProviderInstance.getContractAddress("EtherFiAdmin"))); etherFiOracleInstance = EtherFiOracle(payable(addressProviderInstance.getContractAddress("EtherFiOracle"))); + etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(address(0xDadEf1fFBFeaAB4f68A9fD181395F68b4e4E7Ae0))); + etherFiRestakerInstance = EtherFiRestaker(payable(address(0x1B7a4C3797236A1C37f8741c0Be35c2c72736fFf))); roleRegistryInstance = RoleRegistry(addressProviderInstance.getContractAddress("RoleRegistry")); + + ///remove after steth instant withdrawal is live + upgradeEtherFiRedemptionManager(); + } + + function upgradeEtherFiRedemptionManager() public { + console.log("upgradeEtherFiRedemptionManager"); + address ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + EtherFiRedemptionManagerTemp EtherFiRedemptionManagerTempInstance = EtherFiRedemptionManagerTemp(payable(address(0xDadEf1fFBFeaAB4f68A9fD181395F68b4e4E7Ae0))); + EtherFiRedemptionManagerTemp tempImplementation = new EtherFiRedemptionManagerTemp(address(payable(liquidityPoolInstance)), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistryInstance)); + EtherFiRedemptionManager Implementation = new EtherFiRedemptionManager(address(payable(liquidityPoolInstance)), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistryInstance), address(etherFiRestakerInstance)); + EtherFiRestaker restakerImplementation = new EtherFiRestaker(address(eigenLayerRewardsCoordinator), address(etherFiRedemptionManagerInstance)); + vm.startPrank(owner); + EtherFiRedemptionManagerTempInstance.upgradeTo(address(tempImplementation)); + EtherFiRedemptionManagerTempInstance.clearOutSlotForUpgrade(); + etherFiRestakerInstance.upgradeTo(address(restakerImplementation)); + vm.stopPrank(); + vm.prank(owner); + etherFiRedemptionManagerInstance.upgradeTo(address(Implementation)); + address[] memory _tokens = new address[](2); + _tokens[0] = ETH_ADDRESS; + _tokens[1] = address(etherFiRestakerInstance.lido()); + uint16[] memory _exitFeeSplitToTreasuryInBps = new uint16[](2); + _exitFeeSplitToTreasuryInBps[0] = 10_00; + _exitFeeSplitToTreasuryInBps[1] = 10_00; + uint16[] memory _exitFeeInBps = new uint16[](2); + _exitFeeInBps[0] = 1_00; + _exitFeeInBps[1] = 1_00; + uint16[] memory _lowWatermarkInBpsOfTvl = new uint16[](2); + _lowWatermarkInBpsOfTvl[0] = 1_00; + _lowWatermarkInBpsOfTvl[1] = 50; + uint256[] memory _bucketCapacity = new uint256[](2); + _bucketCapacity[0] = 2000 ether; + _bucketCapacity[1] = 2000 ether; + uint256[] memory _bucketRefillRate = new uint256[](2); + _bucketRefillRate[0] = 0.3 ether; + _bucketRefillRate[1] = 0.3 ether; + vm.startPrank(owner); + roleRegistryInstance.grantRole(keccak256("ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE"), owner); + etherFiRedemptionManagerInstance.initializeTokenParameters(_tokens, _exitFeeSplitToTreasuryInBps, _exitFeeInBps, _lowWatermarkInBpsOfTvl, _bucketCapacity, _bucketRefillRate); + vm.stopPrank(); } function updateShouldSetRoleRegistry(bool shouldSetup) public { @@ -442,7 +486,7 @@ contract TestSetup is Test, ContractCodeChecker, DepositDataGeneration { } function deployEtherFiRestaker() internal { - etherFiRestakerImplementation = new EtherFiRestaker(address(0x1B7a4C3797236A1C37f8741c0Be35c2c72736fFf)); + etherFiRestakerImplementation = new EtherFiRestaker(address(0x1B7a4C3797236A1C37f8741c0Be35c2c72736fFf), address(etherFiRedemptionManagerInstance)); etherFiRestakerProxy = new UUPSProxy(address(etherFiRestakerImplementation), ""); etherFiRestakerInstance = EtherFiRestaker(payable(etherFiRestakerProxy)); @@ -628,16 +672,36 @@ contract TestSetup is Test, ContractCodeChecker, DepositDataGeneration { etherFiOracleProxy = new UUPSProxy(address(etherFiOracleImplementation), ""); etherFiOracleInstance = EtherFiOracle(payable(etherFiOracleProxy)); - etherFiRestakerImplementation = new EtherFiRestaker(address(0x0)); + etherFiRestakerImplementation = new EtherFiRestaker(address(0x0), address(0x0)); etherFiRestakerProxy = new UUPSProxy(address(etherFiRestakerImplementation), ""); etherFiRestakerInstance = EtherFiRestaker(payable(etherFiRestakerProxy)); - etherFiRedemptionManagerProxy = new UUPSProxy(address(new EtherFiRedemptionManager(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistryInstance))), ""); + etherFiRedemptionManagerProxy = new UUPSProxy(address(new EtherFiRedemptionManager(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistryInstance), address(etherFiRestakerInstance))), ""); etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); - etherFiRedemptionManagerInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); + + + address[] memory _tokens = new address[](2); + _tokens[0] = address(etherFiRedemptionManagerInstance.ETH_ADDRESS()); + _tokens[1] = address(etherFiRestakerInstance.lido()); + uint16[] memory _exitFeeSplitToTreasuryInBps = new uint16[](2); + _exitFeeSplitToTreasuryInBps[0] = 10_00; + _exitFeeSplitToTreasuryInBps[1] = 10_00; + uint16[] memory _exitFeeInBps = new uint16[](2); + _exitFeeInBps[0] = 1_00; + _exitFeeInBps[1] = 1_00; + uint16[] memory _lowWatermarkInBpsOfTvl = new uint16[](2); + _lowWatermarkInBpsOfTvl[0] = 1_00; + _lowWatermarkInBpsOfTvl[1] = 1_00; + uint256[] memory _bucketCapacity = new uint256[](2); + _bucketCapacity[0] = 5 ether; + _bucketCapacity[1] = 5 ether; + uint256[] memory _bucketRefillRate = new uint256[](2); + _bucketRefillRate[0] = 0.001 ether; + _bucketRefillRate[1] = 0.001 ether; roleRegistryInstance.grantRole(keccak256("ETHERFI_REDEMPTION_MANAGER_ADMIN_ROLE"), owner); - + etherFiRedemptionManagerInstance.initializeTokenParameters(_tokens, _exitFeeSplitToTreasuryInBps, _exitFeeInBps, _lowWatermarkInBpsOfTvl, _bucketCapacity, _bucketRefillRate); + liquidityPoolInstance.initialize(address(eETHInstance), address(stakingManagerInstance), address(etherFiNodeManagerProxy), address(membershipManagerInstance), address(TNFTInstance), address(etherFiAdminProxy), address(withdrawRequestNFTInstance)); liquidityPoolInstance.initializeVTwoDotFourNine(address(roleRegistryInstance), address(etherFiRedemptionManagerInstance)); @@ -849,7 +913,7 @@ contract TestSetup is Test, ContractCodeChecker, DepositDataGeneration { // upgrade our existing contracts to utilize `roleRegistry` vm.stopPrank(); vm.startPrank(owner); - EtherFiRedemptionManager etherFiRedemptionManagerImplementation = new EtherFiRedemptionManager(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistryInstance)); + EtherFiRedemptionManager etherFiRedemptionManagerImplementation = new EtherFiRedemptionManager(address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance), address(treasuryInstance), address(roleRegistryInstance), address(etherFiRestakerInstance)); etherFiRedemptionManagerProxy = new UUPSProxy(address(etherFiRedemptionManagerImplementation), ""); etherFiRedemptionManagerInstance = EtherFiRedemptionManager(payable(etherFiRedemptionManagerProxy)); etherFiRedemptionManagerInstance.initialize(10_00, 1_00, 1_00, 5 ether, 0.001 ether); // 10% fee split to treasury, 1% exit fee, 1% low watermark