26Q2 - Security Upgrades#385
Conversation
📊 Forge Coverage ReportGenerated by workflow run #731 |
…idityPool is paused; add tests for permissionless claims
…guard feat: Reentrancy Guard and permissonless claim
yash/feat/pausable-until
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bdac379. Configure here.
| if (_etherFiNodesManager && IEtherFiPausable(address(etherFiNodesManager)).paused()) { | ||
| etherFiNodesManager.unPauseContract(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Missing constructor validation for validator approval ceiling
Medium Severity
The constructor validates all immutable ceiling parameters for non-zero (_maxAcceptableRebaseAprInBps, _maxValidatorTaskBatchSize, _staleOracleReportBlockWindow, _maxAcceptableFinalizedWithdrawalAmountPerDay) but omits validation for _maxAcceptableNumValidatorsToApprovePerDay. If deployed with 0, updateMaxNumValidatorsToApprovePerDay can never set maxNumValidatorsToApprovePerDay above 0 (since _maxNumValidatorsToApprovePerDay > 0 always reverts), permanently blocking all validator approvals through _validateValidatorApprovals.
Reviewed by Cursor Bugbot for commit bdac379. Configure here.
…ate-limits Signed-off-by: Pankaj Jagtap <jagtap.pankaj2000@gmail.com>
…limits feat: integrate EtherFi rate limiter into EETH and WeETH contracts
| function unpublishReport(bytes32 _hash) external isAdmin { | ||
| require(consensusStates[_hash].consensusReached, "Consensus is not reached yet"); | ||
| function unpublishReport(bytes32 _hash) external onlyOperations { | ||
| if (!consensusStates[_hash].consensusReached) revert ConsensusNotReached(); |
There was a problem hiding this comment.
Check that the task has not been executed before unpublishing
|
|
||
| function setClaimDelay(uint256 _claimDelay) external { | ||
| if(!roleRegistry.hasRole(CUMULATIVE_MERKLE_REWARDS_DISTRIBUTOR_CLAIM_DELAY_SETTER_ROLE, msg.sender)) revert IncorrectRole(); | ||
| function setClaimDelay(uint256 _claimDelay) external onlyOperations { |
There was a problem hiding this comment.
Should put this behind onlyAdmin, operating timelock
|
|
||
| function updateWhitelistedRecipient(address user, bool isWhitelisted) external { | ||
| if(!roleRegistry.hasRole(CUMULATIVE_MERKLE_REWARDS_DISTRIBUTOR_ADMIN_ROLE, msg.sender)) revert IncorrectRole(); | ||
| function updateWhitelistedRecipient(address user, bool isWhitelisted) external onlyOperations { |
There was a problem hiding this comment.
Should put this behind onlyAdmin, operating timelock
| function revokeEOA1Role(address account) external onlyOperations { | ||
| roleRegistry.revokeFast(roleRegistry.ORACLE_OPERATIONS_ROLE(), account); | ||
| } | ||
|
|
||
| function revokeEOA2Role(address account) external onlyOperations { | ||
| roleRegistry.revokeFast(roleRegistry.HOUSEKEEPING_OPERATIONS_ROLE(), account); | ||
| } | ||
|
|
||
| function revokeEOA3Role(address account) external onlyOperations { | ||
| roleRegistry.revokeFast(roleRegistry.EXECUTOR_OPERATIONS_ROLE(), account); | ||
| } | ||
|
|
||
| function revokeEOA4Role(address account) external onlyOperations { | ||
| roleRegistry.revokeFast(roleRegistry.EIGENPOD_OPERATIONS_ROLE(), account); | ||
| } |
There was a problem hiding this comment.
Rename EOA naming from here
| IBlacklister public immutable blacklister; | ||
| INodeOperatorManager public immutable nodeOperatorManager; | ||
| address public immutable stakingManagerContractAddress; | ||
| address public immutable membershipManagerContractAddress; |
There was a problem hiding this comment.
Import our treasury address here and update the transferAccumulatedRevenue to send the ETH to the Treasury
| IRoleRegistry public immutable roleRegistry; | ||
| IBlacklister public immutable blacklister; | ||
|
|
||
| bytes32 public constant MEMBERSHIP_MANAGER_OPERATIONS_ROLE = keccak256("MEMBERSHIP_MANAGER_OPERATIONS_ROLE"); |
There was a problem hiding this comment.
Should remove this
| /// @title Router token swapping functionality | ||
| /// @notice Functions for swapping tokens via PancakeSwap V3 | ||
| interface IPancackeV3SwapRouter { | ||
| function WETH9() external returns (address); | ||
|
|
||
| struct ExactInputSingleParams { | ||
| address tokenIn; | ||
| address tokenOut; | ||
| uint24 fee; | ||
| address recipient; | ||
| uint256 deadline; | ||
| uint256 amountIn; | ||
| uint256 amountOutMinimum; | ||
| uint160 sqrtPriceLimitX96; | ||
| } | ||
|
|
||
| /// @notice Swaps `amountIn` of one token for as much as possible of another token | ||
| /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance, | ||
| /// and swap the entire amount, enabling contracts to send tokens before calling this function. | ||
| /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata | ||
| /// @return amountOut The amount of the received token | ||
| function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); |
There was a problem hiding this comment.
Let's move this to ILiquifier or remove all together with address private pancakeswapRouter
| interface IERC20Burnable is IERC20 { | ||
| function burn(uint256 amount) external; | ||
| } |
There was a problem hiding this comment.
Same, should more to ILiquiifier. Let's take this easy wins
| } | ||
|
|
||
| function updateTimeBoundCapRefreshInterval(uint32 _timeBoundCapRefreshInterval) external onlyOwner { | ||
| function updateTimeBoundCapRefreshInterval(uint32 _timeBoundCapRefreshInterval) external onlyOperations { |
There was a problem hiding this comment.
Let's put all such setter functions behind onlyAdmin on this contract and remove inconsistency with RedemptionManager and within this contract
|
|
||
| /// Set the claimer of the restaking rewards of this contract | ||
| function setRewardsClaimer(address _claimer) external onlyAdmin { | ||
| function setRewardsClaimer(address _claimer) external onlyOperations { |
There was a problem hiding this comment.
Lets keep this onlyAdmin. We dont want to change the receipient address. We had request to have similar receipient address setter behind Timelock.
|
|
||
| // Send the ETH back to the liquidity pool | ||
| function withdrawEther() public onlyAdmin { | ||
| function withdrawEther() public onlyOperations { |
There was a problem hiding this comment.
This should be houseKeeping role
| } | ||
|
|
||
| function transferAccumulatedRevenue() external onlyAdmin { | ||
| function transferAccumulatedRevenue() external onlyOperations { |
There was a problem hiding this comment.
Housekeeping Role after the change for transfer to Treasury is implemented
| function pauseContract() external onlyOperations { | ||
| _pause(); | ||
| } | ||
| function unPauseContract() external { | ||
| if (!roleRegistry.hasRole(roleRegistry.PROTOCOL_UNPAUSER(), msg.sender)) revert IncorrectRole(); | ||
| function unPauseContract() external onlyOperations { | ||
| _unpause(); | ||
| } |
There was a problem hiding this comment.
We intended to remove this because no pausing functionality in this one
| function completeQueuedWithdrawals( | ||
| IDelegationManager.Withdrawal[] memory _queuedWithdrawals, | ||
| IERC20[][] memory _tokens | ||
| ) external { | ||
| if (!roleRegistry.hasRole(roleRegistry.EXECUTOR_OPERATIONS_ROLE(), msg.sender)) revert IncorrectRole(); |
There was a problem hiding this comment.
HouseKeeping Role
| function stEthClaimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external { | ||
| if (!roleRegistry.hasRole(roleRegistry.EXECUTOR_OPERATIONS_ROLE(), msg.sender)) revert IncorrectRole(); | ||
| lidoWithdrawalQueue.claimWithdrawals(_requestIds, _hints); |
There was a problem hiding this comment.
HouseKeeping Role
| function setProofSubmitter(address node, address proofSubmitter) public onlyEigenlayerAdmin whenNotPaused { | ||
| _validateNode(node); | ||
| IEtherFiNode(node).setProofSubmitter(proofSubmitter); | ||
| } | ||
|
|
There was a problem hiding this comment.
onlyOperations Role
| function setProofSubmitter(uint256 id, address proofSubmitter) external onlyEigenlayerAdmin whenNotPaused { | ||
| setProofSubmitter(etherfiNodeAddress(id), proofSubmitter); | ||
| } |
There was a problem hiding this comment.
onlyOperations
| function queueETHWithdrawal(address node, uint256 amount) public onlyEigenlayerAdmin whenNotPaused returns (bytes32 withdrawalRoot) { | ||
| _validateNode(node); | ||
| rateLimiter.consume(UNRESTAKING_LIMIT_ID, SafeCast.toUint64(amount / 1 gwei)); | ||
| return IEtherFiNode(node).queueETHWithdrawal(amount); | ||
| } | ||
|
|
||
| function queueETHWithdrawal(uint256 id, uint256 amount) external onlyEigenlayerAdmin whenNotPaused returns (bytes32 withdrawalRoot) { | ||
| return queueETHWithdrawal(etherfiNodeAddress(id), amount); | ||
| } |
There was a problem hiding this comment.
Exectuor Operations role
| function queueWithdrawals(address node, IDelegationManager.QueuedWithdrawalParams[] calldata params) public onlyEigenlayerAdmin whenNotPaused { | ||
| _validateNode(node); | ||
| // need to rate limit any beacon eth being withdrawn | ||
| rateLimiter.consume(UNRESTAKING_LIMIT_ID, SafeCast.toUint64(sumRestakingETHWithdrawals(params) / 1 gwei)); | ||
| IEtherFiNode(node).queueWithdrawals(params); | ||
| } | ||
|
|
||
| function queueWithdrawals(uint256 id, IDelegationManager.QueuedWithdrawalParams[] calldata params) external onlyEigenlayerAdmin whenNotPaused { | ||
| queueWithdrawals(etherfiNodeAddress(id), params); | ||
| } |
There was a problem hiding this comment.
Exectuor Operations role
| /// @param _bidId the ID of the validator | ||
| function processAuctionFeeTransfer( | ||
| uint256 _bidId | ||
| ) external onlyStakingManagerContract { | ||
| uint256 amount = bids[_bidId].amount; | ||
| uint256 newAccumulatedRevenue = accumulatedRevenue + amount; | ||
| if (newAccumulatedRevenue >= accumulatedRevenueThreshold) { | ||
| accumulatedRevenue = 0; | ||
| (bool sent, ) = membershipManagerContractAddress.call{value: newAccumulatedRevenue}(""); | ||
| require(sent, "Failed to send Ether"); | ||
| if (!sent) revert EtherTransferFailed(); | ||
| } else { | ||
| accumulatedRevenue = uint128(newAccumulatedRevenue); | ||
| } | ||
| } |
There was a problem hiding this comment.
Not used anywhere. Can just remove it
| uint32 public nextRequestId; | ||
| uint32 public lastFinalizedRequestId; | ||
| uint16 public shareRemainderSplitToTreasuryInBps; | ||
| uint16 public _unused_gap; |
There was a problem hiding this comment.
Let's make this variable private too ?
| function setBlacklistUntil(address user, uint256 until) external onlyOperations { | ||
| blacklistedUntil[user] = block.timestamp + until; | ||
| emit UserBlacklistedUntil(user, block.timestamp + until); | ||
| } |
There was a problem hiding this comment.
Thinking what if we need to blacklist some malicious users across the industry every now and then. It would not be possible to keep doing Multisig transactions with our process. What to do in that case? @0xpanicError @seongyun-ko
|
Recommend to abstract modifiers and pausable functions in the Pausable contract. changes in every contract otherwise |


Note
High Risk
High risk because it modifies access control and pause/upgrade permissions across core contracts (
AuctionManager,EETH,EtherFiAdmin,BucketRateLimiter, etc.) and changes oracle report/withdrawal finalization logic, which can directly impact protocol safety and fund flows.Overview
Security-focused refactor across core contracts. Replaces legacy owner/admin mappings and per-contract role constants with consolidated
RoleRegistrygates (timelock/multisig/guardian/upgrader), addsIBlacklisterenforcement on user-facing paths, and introduces pause-until controls viaPausableUntil(including new guardian/ops pause flows).Protocol behavior tightened.
EtherFiAdminnow validates oracle reports via a staged_validateReportpipeline (APR/fees/withdrawals/validator-approval rate caps) and adds a permissionless stale-oracle escape hatch to finalize withdrawals when reports stop;EETHadds rate-limited mint/burn/transfer paths and pause controls;AuctionManagerandBucketRateLimiterswitch to role-based operations and add clearer custom errors.Ops tooling aligned. Numerous scripts update role references to tier roles, rename timelock delay constants, adjust constructor args/bytecode verification for upgraded implementations, add an Optimism RPC secret to CI, and ignore
foundry.lock.Reviewed by Cursor Bugbot for commit 97cfd1f. Bugbot is set up for automated code reviews on this repo. Configure here.