Skip to content

26Q2 - Security Upgrades#385

Open
pankajjagtapp wants to merge 277 commits into
masterfrom
pankaj/feat/security-upgrades
Open

26Q2 - Security Upgrades#385
pankajjagtapp wants to merge 277 commits into
masterfrom
pankaj/feat/security-upgrades

Conversation

@pankajjagtapp
Copy link
Copy Markdown
Contributor

@pankajjagtapp pankajjagtapp commented Apr 29, 2026

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 RoleRegistry gates (timelock/multisig/guardian/upgrader), adds IBlacklister enforcement on user-facing paths, and introduces pause-until controls via PausableUntil (including new guardian/ops pause flows).

Protocol behavior tightened. EtherFiAdmin now validates oracle reports via a staged _validateReport pipeline (APR/fees/withdrawals/validator-approval rate caps) and adds a permissionless stale-oracle escape hatch to finalize withdrawals when reports stop; EETH adds rate-limited mint/burn/transfer paths and pause controls; AuctionManager and BucketRateLimiter switch 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.

Comment thread src/EtherFiAdmin.sol
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 29, 2026

📊 Forge Coverage Report

Coverage report could not be generated. Check workflow logs for details.

Generated by workflow run #731

Comment thread src/LiquidityPool.sol
Comment thread src/PriorityWithdrawalQueue.sol
Comment thread src/Liquifier.sol
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

❌ 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.

Comment thread src/EtherFiAdmin.sol
if (_etherFiNodesManager && IEtherFiPausable(address(etherFiNodesManager)).paused()) {
etherFiNodesManager.unPauseContract();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bdac379. Configure here.

Comment thread src/EtherFiOracle.sol
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();
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should put this behind onlyAdmin, operating timelock

Comment on lines +29 to +43
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);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename EOA naming from here

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops

Comment thread src/AuctionManager.sol
IBlacklister public immutable blacklister;
INodeOperatorManager public immutable nodeOperatorManager;
address public immutable stakingManagerContractAddress;
address public immutable membershipManagerContractAddress;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import our treasury address here and update the transferAccumulatedRevenue to send the ETH to the Treasury

Comment thread src/MembershipManager.sol
IRoleRegistry public immutable roleRegistry;
IBlacklister public immutable blacklister;

bytes32 public constant MEMBERSHIP_MANAGER_OPERATIONS_ROLE = keccak256("MEMBERSHIP_MANAGER_OPERATIONS_ROLE");
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should remove this

Comment thread src/Liquifier.sol
Comment on lines 21 to 42
/// @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);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to ILiquifier or remove all together with address private pancakeswapRouter

Comment thread src/Liquifier.sol
Comment on lines 47 to 49
interface IERC20Burnable is IERC20 {
function burn(uint256 amount) external;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, should more to ILiquiifier. Let's take this easy wins

Comment thread src/Liquifier.sol
}

function updateTimeBoundCapRefreshInterval(uint32 _timeBoundCapRefreshInterval) external onlyOwner {
function updateTimeBoundCapRefreshInterval(uint32 _timeBoundCapRefreshInterval) external onlyOperations {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put all such setter functions behind onlyAdmin on this contract and remove inconsistency with RedemptionManager and within this contract

Comment thread src/EtherFiRestaker.sol

/// Set the claimer of the restaking rewards of this contract
function setRewardsClaimer(address _claimer) external onlyAdmin {
function setRewardsClaimer(address _claimer) external onlyOperations {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets keep this onlyAdmin. We dont want to change the receipient address. We had request to have similar receipient address setter behind Timelock.

Comment thread src/EtherFiRestaker.sol

// Send the ETH back to the liquidity pool
function withdrawEther() public onlyAdmin {
function withdrawEther() public onlyOperations {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be houseKeeping role

Comment thread src/AuctionManager.sol
}

function transferAccumulatedRevenue() external onlyAdmin {
function transferAccumulatedRevenue() external onlyOperations {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Housekeeping Role after the change for transfer to Treasury is implemented

Comment thread src/StakingManager.sol
Comment on lines +72 to 77
function pauseContract() external onlyOperations {
_pause();
}
function unPauseContract() external {
if (!roleRegistry.hasRole(roleRegistry.PROTOCOL_UNPAUSER(), msg.sender)) revert IncorrectRole();
function unPauseContract() external onlyOperations {
_unpause();
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We intended to remove this because no pausing functionality in this one

Comment thread src/EtherFiRestaker.sol
Comment on lines +268 to +272
function completeQueuedWithdrawals(
IDelegationManager.Withdrawal[] memory _queuedWithdrawals,
IERC20[][] memory _tokens
) external {
if (!roleRegistry.hasRole(roleRegistry.EXECUTOR_OPERATIONS_ROLE(), msg.sender)) revert IncorrectRole();
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HouseKeeping Role

Comment thread src/EtherFiRestaker.sol
Comment on lines +180 to 182
function stEthClaimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external {
if (!roleRegistry.hasRole(roleRegistry.EXECUTOR_OPERATIONS_ROLE(), msg.sender)) revert IncorrectRole();
lidoWithdrawalQueue.claimWithdrawals(_requestIds, _hints);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HouseKeeping Role

Comment on lines 141 to 145
function setProofSubmitter(address node, address proofSubmitter) public onlyEigenlayerAdmin whenNotPaused {
_validateNode(node);
IEtherFiNode(node).setProofSubmitter(proofSubmitter);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onlyOperations Role

Comment on lines 146 to 148
function setProofSubmitter(uint256 id, address proofSubmitter) external onlyEigenlayerAdmin whenNotPaused {
setProofSubmitter(etherfiNodeAddress(id), proofSubmitter);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onlyOperations

Comment on lines 150 to 158
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);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exectuor Operations role

Comment on lines 172 to 181
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);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exectuor Operations role

Comment thread src/AuctionManager.sol
Comment on lines 224 to 237
/// @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);
}
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not used anywhere. Can just remove it

uint32 public nextRequestId;
uint32 public lastFinalizedRequestId;
uint16 public shareRemainderSplitToTreasuryInBps;
uint16 public _unused_gap;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this variable private too ?

Comment on lines +40 to +43
function setBlacklistUntil(address user, uint256 until) external onlyOperations {
blacklistedUntil[user] = block.timestamp + until;
emit UserBlacklistedUntil(user, block.timestamp + until);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@pankajjagtapp
Copy link
Copy Markdown
Contributor Author

Recommend to abstract modifiers and pausable functions in the Pausable contract. changes in every contract otherwise

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants