diff --git a/script/deploys/Deployed_Hoodi.s.sol b/script/deploys/Deployed_Hoodi.s.sol new file mode 100644 index 00000000..ee2bbfaa --- /dev/null +++ b/script/deploys/Deployed_Hoodi.s.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +/** + * @title Deployed_Hoodi + * @notice Contains addresses of all deployed contracts on Hoodi testnet + */ +contract Deployed_Hoodi { + // Core Protocol Contracts + address public constant ADDRESS_PROVIDER = 0xd4bBb3Ba0827Ed7abC6977C572910d25a1488296; + address public constant LIQUIDITY_POOL = 0x4a8081095549e63153a61D21F92ff079fe39858E; + address public constant ETHFI = address(0); + address public constant EETH = 0x5595b182162DB7ECfdFE5Ea948d7636b9e250C4D; + address public constant WEETH = 0xd5A50FAE2736CA59Bd6Ac4AF59b1f0fFAB62c4A2; + + // Membership & NFTs + address public constant MEMBERSHIP_MANAGER = 0x79eF7d2d9b68056912Eb020ac65b971017191DE0; + address public constant MEMBERSHIP_NFT = 0x254eAD7aca562D50624b0556729Ca9843b7f6FbB; + address public constant TNFT = 0xd31bC004Ba46A048e272A45A6b24Ed985c4DF5AC; + address public constant BNFT = 0x2B736C58EE03C5d4930a32D3c8F6acd7FbbdA08C; + address public constant WITHDRAW_REQUEST_NFT = 0xb17528f26b0F7ED107E4E17f48bcC2E169Dcb6c1; + + // Staking & Withdrawals + address public constant NODE_OPERATOR_MANAGER = 0x3e17543CaE3366cc67a3CBeD5Aa42d9d09D59b39; + address public constant AUCTION_MANAGER = 0x261315c176864cE29D582f38DdA4930ED17CD95A; + address public constant STAKING_MANAGER = 0xDbE50E32Ed95f539F36bA315a75377FBc35aBc12; + address public constant ETHERFI_NODE_BEACON = 0x7AbD4dF572a4Daaed21b1FdaDE897a5A634a1fd1; + address public constant ETHERFI_NODES_MANAGER = 0x7579194b8265e3Aa7df451c6BD2aff5B1FC5F945; + address public constant ETHERFI_REDEMPTION_MANAGER = 0x95AeCaa1B0C3A04C8aFf5D05f27363e9e3367D6F; + + // Oracle + address public constant ETHERFI_ORACLE = 0x1888Fd1914af6980204AA0424f550d9bE35735e1; + address public constant ETHERFI_ADMIN = 0x0CF5ddcF6861Efd8C498466d162F231E44eB85Dd; + + // Adapters & Liquifiers + address public constant DEPOSIT_ADAPTER = address(0); // MISSING + address public constant LIQUIFIER = 0x2e871581aAcc79EbcF75F9da364f5078FAd9bb4D; + + // AVS & Sync + address public constant ETHERFI_RESTAKER = 0xc27F4dae10Ec60539619F7Deb0E2dBb413df6EAd; + address public constant ETHERFI_AVS_OPERATORS_MANAGER = address(0); // MISSING + address public constant ETHERFI_L1_SYNC_POOL_ETH = address(0); + address public constant ETHERFI_OFT_ADAPTER = address(0); + + // Utilities + address public constant ETHERFI_VIEWER = 0xA239C957951C2237eCd730596629b246E2c75857; + address public constant ETHERFI_REWARDS_ROUTER = 0x703f2f1eC0B82EFe1C16927aEbc4D99536ECF5CE; + address public constant ETHERFI_OPERATION_PARAMETERS = 0x01bc3a772307394E755a3519E6983fEA445B2722; + address public constant ETHERFI_RATE_LIMITER = 0x1e6881572e7bB49B4737ac650bce5587085a4d48; + + address public constant EARLY_ADOPTER_POOL = address(0); + + // role registry & multi-sig + address public constant ROLE_REGISTRY = 0x7279853cA1804d4F705d885FeA7f1662323B5Aab; + address public constant UPGRADE_TIMELOCK = address(0); + address public constant OPERATING_TIMELOCK = address(0); + address public constant ETHERFI_OPERATING_ADMIN = address(0); + address public constant ETHERFI_UPGRADE_ADMIN = address(0); + + // Additional Hoodi-specific contracts + address public constant TREASURY = 0xa16E2fcf1331B2AA90b3a83EC0B54923d74b5E19; + address public constant ETHERFI_TIMELOCK = 0x75AEB07F913a895F1eE2e0a8990B633D1dB00731; + address public constant PROTOCOL_REVENUE_MANAGER = 0xA7C53aCCBB67D803e185E63730BB78C68db2966d; + address public constant REGULATIONS_MANAGER = 0x91E4e2c24f8634f05a46dd88F8d79cA0767575f6; + address public constant BUCKET_RATE_LIMITER = 0x52DbeF0e3E019aafbB654bc80c15ffa4Dcc17566; + address public constant TVL_ORACLE = 0x9B9D42E2D3B3989567de5028A91d9492B8cF68c2; + address public constant ETHERFI_NODE = 0xCb77c1EDf717b551C57c15332700b213c02f1b90; + address public constant MAIN_ADMIN = 0x001000621b95AA950c1a27Bb2e1273e10d8dfF68; + + // External contracts (EigenLayer, Ethereum) + address public constant ETH2_DEPOSIT = 0x00000000219ab540356cBB839Cbe05303d7705Fa; + address public constant EIGEN_POD_MANAGER = 0xcd1442415Fc5C29Aa848A49d2e232720BE07976c; + address public constant DELEGATION_MANAGER = 0x867837a9722C512e0862d8c2E15b8bE220E8b87d; + address public constant REWARDS_COORDINATOR = 0x29e8572678e0c272350aa0b4B8f304E47EBcd5e7; + address public constant BEACON_ORACLE = 0x5e1577f8efB21b229cD5Eb4C5Aa3d6C4b228f650; + address public constant STRATEGY_MANAGER = 0xeE45e76ddbEDdA2918b8C7E3035cd37Eab3b5D41; + address public constant CREATE2_FACTORY_HOODI = 0x29bd9fc3E826f10288D58bEa41d1258FB3ecF4F0; + + mapping(address => address) public timelockToAdmin; + + constructor() { + } +} diff --git a/script/hoodi/EtherFiRedemptionManagerTemp.sol b/script/hoodi/EtherFiRedemptionManagerTemp.sol new file mode 100644 index 00000000..46e76bd0 --- /dev/null +++ b/script/hoodi/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 "../../src/interfaces/ILiquidityPool.sol"; +import "../../src/interfaces/IeETH.sol"; +import "../../src/interfaces/IWeETH.sol"; + +import "lib/BucketLimiter.sol"; + +import "../../src/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/script/hoodi/Hoodi-Salts.s.sol b/script/hoodi/Hoodi-Salts.s.sol new file mode 100644 index 00000000..c8dc346d --- /dev/null +++ b/script/hoodi/Hoodi-Salts.s.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/** + * @title Hoodi-Salts + * @notice Salt values for Create2 deployments on Hoodi testnet + * @dev These salts ensure deterministic addresses across deployments + */ +contract HoodiSalts { + // Core Protocol Contracts + bytes32 public constant TREASURY_SALT = keccak256("Treasury"); + bytes32 public constant NODE_OPERATOR_MANAGER_SALT = keccak256("NodeOperatorManager"); + bytes32 public constant AUCTION_MANAGER_SALT = keccak256("AuctionManager"); + bytes32 public constant STAKING_MANAGER_SALT = keccak256("StakingManager"); + bytes32 public constant ETHERFI_NODE_SALT = keccak256("EtherFiNode"); + bytes32 public constant BNFT_SALT = keccak256("BNFT"); + bytes32 public constant TNFT_SALT = keccak256("TNFT"); + bytes32 public constant PROTOCOL_REVENUE_MANAGER_SALT = keccak256("ProtocolRevenueManager"); + bytes32 public constant ETHERFI_NODES_MANAGER_SALT = keccak256("EtherFiNodesManager"); + bytes32 public constant REGULATIONS_MANAGER_SALT = keccak256("RegulationsManager"); + bytes32 public constant MEMBERSHIP_MANAGER_SALT = keccak256("MembershipManager"); + bytes32 public constant WITHDRAW_REQUEST_NFT_SALT = keccak256("WithdrawRequestNFT"); + bytes32 public constant MEMBERSHIP_NFT_SALT = keccak256("MembershipNFT"); + bytes32 public constant LIQUIDITY_POOL_SALT = keccak256("LiquidityPool"); + bytes32 public constant EETH_SALT = keccak256("EETH"); + bytes32 public constant WEETH_SALT = keccak256("WeETH"); + bytes32 public constant ETHERFI_ORACLE_SALT = keccak256("EtherFiOracle"); + bytes32 public constant ETHERFI_ADMIN_SALT = keccak256("EtherFiAdmin"); + bytes32 public constant ROLE_REGISTRY_SALT = keccak256("RoleRegistry"); + bytes32 public constant ETHERFI_OPERATION_PARAMETERS_SALT = keccak256("EtherFiOperationParameters"); + bytes32 public constant BUCKET_RATE_LIMITER_SALT = keccak256("BucketRateLimiter"); + bytes32 public constant TVL_ORACLE_SALT = keccak256("TVLOracle"); + bytes32 public constant ETHERFI_TIMELOCK_SALT = keccak256("EtherFiTimelock"); + bytes32 public constant LIQUIFIER_SALT = keccak256("Liquifier"); + bytes32 public constant ETHERFI_RESTAKER_SALT = keccak256("EtherFiRestaker"); + bytes32 public constant ETHERFI_REWARDS_ROUTER_SALT = keccak256("EtherFiRewardsRouter"); + bytes32 public constant ADDRESS_PROVIDER_SALT = keccak256("AddressProvider"); + bytes32 public constant ETHERFI_VIEWER_SALT = keccak256("EtherFiViewer"); + bytes32 public constant ETHERFI_REDEMPTION_MANAGER_SALT = keccak256("EtherFiRedemptionManager"); +} + diff --git a/script/hoodi/Upgrade-hoodi.s.sol b/script/hoodi/Upgrade-hoodi.s.sol new file mode 100644 index 00000000..4204c593 --- /dev/null +++ b/script/hoodi/Upgrade-hoodi.s.sol @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/** + * @title UpgradeHoodi + * @notice Script to upgrade contracts on Hoodi testnet that need upgrades using Create2Factory + * + * @dev Usage: + * 1. Ensure contracts are compiled: `forge build` + * 2. Ensure Create2Factory is deployed at CREATE2_FACTORY_HOODI address + * 3. Set HOODI_RPC_URL environment variable or use --fork-url flag + * 4. Set HOODI_PRIVATE_KEY environment variable (must be protocol upgrader for most contracts, owner for some) + * 5. Run: `forge script script/hoodi/Upgrade-hoodi.s.sol --fork-url $HOODI_RPC_URL -vvvv` + */ + +import "forge-std/Script.sol"; +import "forge-std/console2.sol"; +import {Deployed_Hoodi} from "../deploys/Deployed_Hoodi.s.sol"; +import {HoodiSalts} from "./Hoodi-Salts.s.sol"; +import {IRoleRegistry} from "../../src/interfaces/IRoleRegistry.sol"; +import {IStakingManager} from "../../src/interfaces/IStakingManager.sol"; +import {StakingManager} from "../../src/StakingManager.sol"; +import {EtherFiNodesManager} from "../../src/EtherFiNodesManager.sol"; +import {EtherFiRedemptionManager} from "../../src/EtherFiRedemptionManager.sol"; +import {EtherFiNode} from "../../src/EtherFiNode.sol"; +import {EtherFiRedemptionManagerTemp} from "./EtherFiRedemptionManagerTemp.sol"; + +interface IUpgradeable { + function upgradeTo(address newImplementation) external; +} + +interface ICreate2Factory { + function deploy(bytes memory code, string memory contractName) external payable returns (address); +} + +contract UpgradeHoodi is Script, Deployed_Hoodi, HoodiSalts { + // Create2Factory for deterministic deployments + ICreate2Factory constant factory = ICreate2Factory(CREATE2_FACTORY_HOODI); + + // Contract addresses that need upgrades + address constant STAKING_MANAGER_PROXY = 0xDbE50E32Ed95f539F36bA315a75377FBc35aBc12; + address constant ETHERFI_NODES_MANAGER_PROXY = 0x7579194b8265e3Aa7df451c6BD2aff5B1FC5F945; + address constant ETHERFI_REDEMPTION_MANAGER_PROXY = 0x95AeCaa1B0C3A04C8aFf5D05f27363e9e3367D6F; + + // Contract names for Create2Factory (must match Hoodi-Salts.s.sol) + string constant STAKING_MANAGER_NAME = "StakingManager"; + string constant ETHERFI_NODE_NAME = "EtherFiNode"; + string constant ETHERFI_NODES_MANAGER_NAME = "EtherFiNodesManager"; + string constant ETHERFI_REDEMPTION_MANAGER_NAME = "EtherFiRedemptionManager"; + string constant ETHERFI_REDEMPTION_MANAGER_TEMP_NAME = "EtherFiRedemptionManagerTemp"; + + struct UpgradeResult { + string name; + bool upgraded; + string reason; + } + + UpgradeResult[] public upgradeResults; + + // Deployer address (set in run()) + address public deployer; + + function run() external { + // Fork Hoodi testnet + string memory rpc; + try vm.envString("HOODI_RPC_URL") returns (string memory envRpc) { + rpc = envRpc; + } catch { + try vm.rpcUrl("hoodi") returns (string memory configRpc) { + rpc = configRpc; + } catch { + revert("Please set HOODI_RPC_URL env var, add 'hoodi' to foundry.toml rpc_endpoints, or use --fork-url flag"); + } + } + vm.createSelectFork(rpc); + + uint256 deployerPrivateKey = vm.envUint("HOODI_PRIVATE_KEY"); + deployer = vm.addr(deployerPrivateKey); + + console2.log("========================================"); + console2.log("Upgrading Hoodi Contracts (Create2)"); + console2.log("========================================"); + console2.log("Deployer: %s", vm.toString(deployer)); + console2.log("Create2Factory: %s", vm.toString(address(factory))); + console2.log(""); + + // Get RoleRegistry to check protocol upgrader + IRoleRegistry roleRegistry = IRoleRegistry(ROLE_REGISTRY); + address protocolUpgrader = roleRegistry.owner(); + console2.log("Protocol Upgrader (RoleRegistry owner): %s", vm.toString(protocolUpgrader)); + + // Check if deployer is protocol upgrader + bool isProtocolUpgrader = (deployer == protocolUpgrader); + console2.log("Deployer is protocol upgrader: %s\n", isProtocolUpgrader ? "YES" : "NO"); + + vm.startBroadcast(deployerPrivateKey); + // vm.startPrank(deployer); + console2.log("Broadcasting from: %s", vm.toString(deployer)); + + // Upgrade contracts that require protocol upgrader + if (isProtocolUpgrader) { + upgradeStakingManager(); + upgradeEtherFiNodesManager(); + upgradeEtherFiRedemptionManager(); + upgradeEtherFiNodeBeacon(); + } else { + console2.log("[SKIP] Skipping protocol upgrader contracts (deployer is not protocol upgrader)\n"); + } + + vm.stopBroadcast(); + // vm.stopPrank(); + + // Print summary + printSummary(); + } + + // Protocol Upgrader Contracts + function upgradeStakingManager() internal { + console2.log("Upgrading STAKING_MANAGER..."); + address impl = _deployStakingManager(); + IUpgradeable(STAKING_MANAGER_PROXY).upgradeTo(impl); + console2.log(" [OK] STAKING_MANAGER upgraded: %s\n", vm.toString(impl)); + upgradeResults.push(UpgradeResult("STAKING_MANAGER", true, "")); + } + + function upgradeEtherFiNodesManager() internal { + console2.log("Upgrading ETHERFI_NODES_MANAGER..."); + address impl = _deployEtherFiNodesManager(); + IUpgradeable(ETHERFI_NODES_MANAGER_PROXY).upgradeTo(impl); + console2.log(" [OK] ETHERFI_NODES_MANAGER upgraded: %s\n", vm.toString(impl)); + upgradeResults.push(UpgradeResult("ETHERFI_NODES_MANAGER", true, "")); + } + + function upgradeEtherFiRedemptionManager() internal { + address tempImpl = _deployEtherFiRedemptionManagerTemp(); + IUpgradeable(ETHERFI_REDEMPTION_MANAGER_PROXY).upgradeTo(tempImpl); + console2.log(" [OK] ETHERFI_REDEMPTION_MANAGER_TEMP upgraded: %s\n", vm.toString(tempImpl)); + + console2.log("Clearnig out slot for upgrade..."); + EtherFiRedemptionManagerTemp(payable(ETHERFI_REDEMPTION_MANAGER_PROXY)).clearOutSlotForUpgrade(); + console2.log(" [OK] ETHERFI_REDEMPTION_MANAGER_TEMP slot cleared \n"); + + console2.log("Upgrading ETHERFI_REDEMPTION_MANAGER..."); + address impl = _deployEtherFiRedemptionManager(); + IUpgradeable(ETHERFI_REDEMPTION_MANAGER_PROXY).upgradeTo(impl); + console2.log(" [OK] ETHERFI_REDEMPTION_MANAGER upgraded: %s\n", vm.toString(impl)); + upgradeResults.push(UpgradeResult("ETHERFI_REDEMPTION_MANAGER", true, "")); + } + + function upgradeEtherFiNodeBeacon() internal { + console2.log("Upgrading ETHERFI_NODE_BEACON..."); + address impl = _deployEtherFiNode(); + IStakingManager(STAKING_MANAGER_PROXY).upgradeEtherFiNode(impl); + console2.log(" [OK] ETHERFI_NODE_BEACON upgraded: %s\n", vm.toString(impl)); + upgradeResults.push(UpgradeResult("ETHERFI_NODE_BEACON", true, "")); + } + + // Internal deployment functions + + function _deployStakingManager() internal returns (address) { + bytes memory constructorArgs = abi.encode( + LIQUIDITY_POOL, + ETHERFI_NODES_MANAGER, + ETH2_DEPOSIT, + AUCTION_MANAGER, + ETHERFI_NODE_BEACON, + ROLE_REGISTRY + ); + bytes memory bytecode = abi.encodePacked( + type(StakingManager).creationCode, + constructorArgs + ); + + bytes32 salt = keccak256(abi.encodePacked(STAKING_MANAGER_NAME)); + bytes32 initCodeHash = keccak256(bytecode); + address predictedAddress = vm.computeCreate2Address(salt, initCodeHash, address(factory)); + console2.log(" Predicted address: %s", vm.toString(predictedAddress)); + + // Check if contract already exists at this address + uint256 codeSize; + assembly { + codeSize := extcodesize(predictedAddress) + } + if (codeSize > 0) { + console2.log(" Contract already deployed, skipping deployment"); + return predictedAddress; + } + + address deployedAddress = factory.deploy(bytecode, STAKING_MANAGER_NAME); + require(deployedAddress == predictedAddress, "Deployment address mismatch"); + console2.log(" Deployed address: %s", vm.toString(deployedAddress)); + return deployedAddress; + } + + function _deployEtherFiNodesManager() internal returns (address) { + bytes memory constructorArgs = abi.encode( + STAKING_MANAGER, + ROLE_REGISTRY, + ETHERFI_RATE_LIMITER + ); + bytes memory bytecode = abi.encodePacked( + type(EtherFiNodesManager).creationCode, + constructorArgs + ); + + bytes32 salt = keccak256(abi.encodePacked(ETHERFI_NODES_MANAGER_NAME)); + bytes32 initCodeHash = keccak256(bytecode); + address predictedAddress = vm.computeCreate2Address(salt, initCodeHash, address(factory)); + console2.log(" Predicted address: %s", vm.toString(predictedAddress)); + + // Check if contract already exists at this address + uint256 codeSize; + assembly { + codeSize := extcodesize(predictedAddress) + } + if (codeSize > 0) { + console2.log(" Contract already deployed, skipping deployment"); + return predictedAddress; + } + + address deployedAddress = factory.deploy(bytecode, ETHERFI_NODES_MANAGER_NAME); + require(deployedAddress == predictedAddress, "Deployment address mismatch"); + console2.log(" Deployed address: %s", vm.toString(deployedAddress)); + return deployedAddress; + } + + function _deployEtherFiRedemptionManagerTemp() internal returns (address) { + bytes memory constructorArgs = abi.encode( + LIQUIDITY_POOL, + EETH, + WEETH, + TREASURY, + ROLE_REGISTRY + ); + bytes memory bytecode = abi.encodePacked( + type(EtherFiRedemptionManagerTemp).creationCode, + constructorArgs + ); + + bytes32 salt = keccak256(abi.encodePacked(ETHERFI_REDEMPTION_MANAGER_TEMP_NAME)); + bytes32 initCodeHash = keccak256(bytecode); + address predictedAddress = vm.computeCreate2Address(salt, initCodeHash, address(factory)); + console2.log(" Predicted address: %s", vm.toString(predictedAddress)); + + // Check if contract already exists at this address + uint256 codeSize; + assembly { + codeSize := extcodesize(predictedAddress) + } + if (codeSize > 0) { + console2.log(" Contract already deployed, skipping deployment"); + return predictedAddress; + } + address deployedAddress = factory.deploy(bytecode, ETHERFI_REDEMPTION_MANAGER_TEMP_NAME); + require(deployedAddress == predictedAddress, "Deployment address mismatch"); + console2.log(" Deployed address: %s", vm.toString(deployedAddress)); + return deployedAddress; + } + + function _deployEtherFiRedemptionManager() internal returns (address) { + bytes memory constructorArgs = abi.encode( + LIQUIDITY_POOL, + EETH, + WEETH, + TREASURY, + ROLE_REGISTRY, + ETHERFI_RESTAKER + ); + bytes memory bytecode = abi.encodePacked( + type(EtherFiRedemptionManager).creationCode, + constructorArgs + ); + + bytes32 salt = keccak256(abi.encodePacked(ETHERFI_REDEMPTION_MANAGER_NAME)); + bytes32 initCodeHash = keccak256(bytecode); + address predictedAddress = vm.computeCreate2Address(salt, initCodeHash, address(factory)); + console2.log(" Predicted address: %s", vm.toString(predictedAddress)); + + // Check if contract already exists at this address + uint256 codeSize; + assembly { + codeSize := extcodesize(predictedAddress) + } + if (codeSize > 0) { + console2.log(" Contract already deployed, skipping deployment"); + return predictedAddress; + } + + address deployedAddress = factory.deploy(bytecode, ETHERFI_REDEMPTION_MANAGER_NAME); + require(deployedAddress == predictedAddress, "Deployment address mismatch"); + console2.log(" Deployed address: %s", vm.toString(deployedAddress)); + return deployedAddress; + } + + function _deployEtherFiNode() internal returns (address) { + bytes memory constructorArgs = abi.encode( + LIQUIDITY_POOL, + ETHERFI_NODES_MANAGER, + EIGEN_POD_MANAGER, + DELEGATION_MANAGER, + ROLE_REGISTRY + ); + bytes memory bytecode = abi.encodePacked( + type(EtherFiNode).creationCode, + constructorArgs + ); + + bytes32 salt = keccak256(abi.encodePacked(ETHERFI_NODE_NAME)); + bytes32 initCodeHash = keccak256(bytecode); + address predictedAddress = vm.computeCreate2Address(salt, initCodeHash, address(factory)); + console2.log(" Predicted address: %s", vm.toString(predictedAddress)); + + // Check if contract already exists at this address + uint256 codeSize; + assembly { + codeSize := extcodesize(predictedAddress) + } + if (codeSize > 0) { + console2.log(" Contract already deployed, skipping deployment"); + return predictedAddress; + } + + address deployedAddress = factory.deploy(bytecode, ETHERFI_NODE_NAME); + require(deployedAddress == predictedAddress, "Deployment address mismatch"); + console2.log(" Deployed address: %s", vm.toString(deployedAddress)); + return deployedAddress; + } + + function printSummary() internal view { + console2.log("\n========================================"); + console2.log("Upgrade Summary"); + console2.log("========================================"); + + uint256 successCount = 0; + uint256 failCount = 0; + uint256 skipCount = 0; + + for (uint256 i = 0; i < upgradeResults.length; i++) { + UpgradeResult memory result = upgradeResults[i]; + if (result.upgraded) { + console2.log("[OK] %s", result.name); + successCount++; + } else if (bytes(result.reason).length > 0) { + console2.log("[%s] %s: %s", + keccak256(bytes(result.reason)) == keccak256(bytes("Not owner")) ? "SKIP" : "FAIL", + result.name, + result.reason + ); + if (keccak256(bytes(result.reason)) == keccak256(bytes("Not owner"))) { + skipCount++; + } else { + failCount++; + } + } else { + console2.log("[FAIL] %s", result.name); + failCount++; + } + } + + console2.log("\nTotal: %d upgraded, %d failed, %d skipped", successCount, failCount, skipCount); + } +}