fix(security-upgrades): review fixes for #420 — RR ordering, EFRM treasury, deterministic salts, eyeball block#442
Merged
0xpanicError merged 5 commits intoMay 28, 2026
Conversation
…stic salts, eyeball block Addresses findings from PR #420 6-lens review: C1: Reorder upgrade batch. RoleRegistry impl swap moved from FIRST to AFTER the 21 proxy upgrades + beacon, BEFORE the 2 onlyUpgradeTimelock initializers. Old proxy impls gate _authorizeUpgrade via roleRegistry.onlyProtocolUpgrader, which the new RR drops. Swapping RR first would brick every subsequent upgradeTo with a fallback revert. Confirmed via on-chain selector scan of 4 mainnet impls (LP/EFRM/EFNM/WRN all contain 0x5006bb7b, none contain 0x6ac5f9eb). C2: EFRM _treasury constructor arg now WITHDRAW_REQUEST_NFT_BUYBACK_SAFE in BOTH deploy.s.sol (already correct) and transactions.s.sol bytecode-verify + immutable-assert (was TREASURY, would break verifyDeployedBytecode). Matches current mainnet EFRM.treasury() = 0x2f5301..78f0F. Constant name unified to RM_MAX_EXIT_FEE_SPLIT_TO_TREASURY_BPS across both files (matches contract field maxExitFeeSplitToTreasuryInBps). C3+C7: GIT_COMMIT_SHA TBD placeholder + deterministic timelock salts. Replaces bytes32(0) placeholder with explicit bytes20 GIT_COMMIT_SHA = TBD; must be set to first 20 bytes of release commit before broadcast. commitHashSalt derived from it; preflight rejects bytes20(0) in all 3 scripts. Timelock salts now keccak256(abi.encode("batch-{1,2,revert}", commitHashSalt)) — deterministic across re-runs, no more block.number drift between fork dry-run and broadcast. C4: LP_MAX_WITHDRAW_AMOUNT bumped from 1_000 ether (dummy) to 3_000 ether. C6: ADMIN_STALE_ORACLE_REPORT_BLOCK_WINDOW = 7200 * 14 (14 days; previously value 7200*7 with comment claiming 14 days). H3: New _printPleaseEyeball() block at top of run() in both deploy.s.sol and transactions.s.sol prints every TBD constant — GIT_COMMIT_SHA, all 26 deployed impl addresses, all 9 HOLDER_*, 10 rate-limit caps/refills, 11 pause durations, finalized-withdrawal limit, LP bounds, WNFT band — BEFORE preflight runs, so signers can eyeball values in one place. H4: WNFT share-rate band tightened from [1, 4 ether] to [0.95 ether, 1.15 ether] — ~[0.9, 1.1] x live amountPerShareCeil() snapshot (~1.05). Preflight asserts MIN > 0 and MAX > MIN. H5: LIQUIFIER_STALE_PRICE_WINDOW = 1 days (was 7 days; Chainlink stETH/ETH heartbeat is 24h). H8: New _preflightRoleHashes() instantiates new RoleRegistry(revokeAdminProxy) and cross-checks all 9 hardcoded keccak256(role-string) constants against the RR getters. Catches typos in role strings BEFORE any Safe JSON is written. Operational items still on signer's plate before broadcast: GIT_COMMIT_SHA, 6 HOLDER_* placeholders, 20 rate-limit gwei values, 11 pause durations, finalized-withdrawal setpoint, re-verify WNFT band centers on live rate. The PLEASE EYEBALL block surfaces all of them on every run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…ation Verified PR #420 end-to-end on a Tenderly VNet forked from mainnet (31698b4b-77c5-4b96-a057-855ccaac67b8). All 3 batches landed clean: schedule both 10d/2d timelocks together, warp +10d, execute, then the instant LP-bounds batch. 7 of 8 user-facing QA scenarios passed (deposit / wrap / transfer / requestWithdraw / blacklist / unblacklist / pauseContractUntil); two real bugs surfaced and are fixed here, one operational follow-up is documented. F1 (transactions.s.sol _auctionImmSels): new AuctionManager dropped membershipManagerContractAddress(). It existed pre-upgrade so the _safeSnapshot filter let it through; _postSnap then reverted with "previously-surviving selector now reverts". Removed from the selector list (6 -> 5 entries). F2 (transactions.s.sol executeOperatingConfig): UNRESTAKING / EXIT_REQUEST / CONSOLIDATION_REQUEST rate-limit buckets already exist on mainnet from a prior deployment (limitExists() == true). PR #420 unconditionally called createNewLimiter for all 10 buckets -> LimitAlreadyExists() on these 3, atomic batch revert. Switched the 3 EFNM buckets to setCapacity + setRefillRate; the 7 token + restaker buckets still use createNewLimiter (they are genuinely new). The existing consumer wiring for the 3 EFNM buckets is preserved, so no updateConsumer call is needed for them. F3 (ROLE_MIGRATION.md §6.1, operational - no script change): EtherFiRedemptionManager.tokenToRedemptionInfo[EETH/WEETH] defaults to zero after the upgrade. redeemEEth / redeemWeEth revert until OPERATION_TIMELOCK calls initializeTokenParameters([EETH, WEETH], ...). Documented as a Day-10+ operational follow-up alongside H7 (per-address rate-limit pre-seeding) and stale-holder hygiene. Also (deploy.s.sol): set logging=false on LiquidityPool, Liquifier, and EtherFiAdmin deploys because Utils.formatStaticParam reverts with "Unsupported static type" on the ConstructorAddresses struct argument. Deployment still succeeds; only the JSON pretty-print is skipped for these 3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…://github.com/etherfi-protocol/smart-contracts into seongyun/fix/security-upgrades-scripts-review
0xpanicError
approved these changes
May 28, 2026
bc3bee3
into
pankaj/feat/security-upgrades-PR-scripts
1 check passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Addresses findings from a 6-lens review of #420 (two confirmed showstoppers + five more critical + three high-severity), then end-to-end verified on a Tenderly VNet fork of mainnet with all 3 batches landed and 7 user-facing QA scenarios passing.
Review write-up: Notion — PR #420 6-Lens Review.
Targets
pankaj/feat/security-upgrades-PR-scriptsso it can be merged into #420 before broadcast.What's in this PR
Review-driven fixes (commit
e576b2c4)_authorizeUpgradeviaroleRegistry.onlyProtocolUpgrader, which the new RR drops. Swapping RR first bricks every subsequentupgradeTowith a fallback revert. Confirmed via on-chain selector scan of 4 mainnet impls (LP / EFRM / EFNM / WRN — all contain0x5006bb7b, none contain0x6ac5f9eb).transactions.s.sol_treasuryconstructor arg =WITHDRAW_REQUEST_NFT_BUYBACK_SAFEin both files (wasTREASURYin transactions.s.sol bytecode-verify + immutable-assert, would have brokenverifyDeployedBytecode). Matches current mainnetEFRM.treasury() = 0x2f5301…F0F. Constant name unified toRM_MAX_EXIT_FEE_SPLIT_TO_TREASURY_BPS.bytes20 GIT_COMMIT_SHATBD placeholder in all 3 scripts.commitHashSalt = bytes32(GIT_COMMIT_SHA). Preflight rejectsbytes20(0). Timelock salts →keccak256(abi.encode("batch-1"|"batch-2"|"batch-revert", commitHashSalt))— deterministic across re-runs, no moreblock.numberdrift.LP_MAX_WITHDRAW_AMOUNT = 3_000 ether(was 1_000 ether dummy).ADMIN_STALE_ORACLE_REPORT_BLOCK_WINDOW = 7200 * 14(14 days; comment previously claimed 14d but value was 7d)._printPleaseEyeball()at top ofrun()in both deploy and transactions. PrintsGIT_COMMIT_SHA, all 26 deployed impl addresses, all 9HOLDER_*, 10 rate-limit caps/refills, 11 pause durations, finalized-withdrawal limit, LP bounds, WNFT band — before preflight runs.[1, 4 ether]→[0.95 ether, 1.15 ether](≈ [0.9×, 1.1×] of liveamountPerShareCeil()~1.05). Preflight asserts MIN > 0 and MAX > MIN.LIQUIFIER_STALE_PRICE_WINDOW = 1 days(was 7 days)._preflightRoleHashes()instantiatesnew RoleRegistry(revokeAdminProxy)and cross-checks all 9 hardcodedkeccak256role-IDs against RR getters. Catches typos before any Safe JSON is written.Findings from Tenderly VNet verification (commit
3e6d5fa9)VNet
31698b4b-77c5-4b96-a057-855ccaac67b8(fork of mainnet @ 25193922). Randeploy.s.sol --broadcastthen submitted all 5 Safe JSONs viacast rpc eth_sendTransactionwith Tenderly impersonation. Two real bugs surfaced; both fixed in this commit.F1 —
_auctionImmSels()lists a removed selector (transactions.s.sol:995-1005)The new AuctionManager dropped
membershipManagerContractAddress(). It existed pre-upgrade so the_safeSnapshotfilter didn't strip it;_postSnap's strict re-call then reverts with "previously-surviving selector now reverts". Fix: remove from the selector list (6 → 5 entries).F2 — 3 EFNM rate-limit buckets already exist on mainnet (transactions.s.sol:1738-1757)
UNRESTAKING_LIMIT_ID,EXIT_REQUEST_LIMIT_ID,CONSOLIDATION_REQUEST_LIMIT_IDwere configured in a prior deployment. PR #420's Batch 2 blindly calledcreateNewLimiterfor all 10 →LimitAlreadyExists(), atomic batch revert. Fix: switch the 3 EFNM buckets tosetCapacity + setRefillRate; the 7 token + restaker buckets still usecreateNewLimiter(genuinely new). Existing consumer wiring for the EFNM 3 is preserved.F3 — Operational follow-up, not in script (ROLE_MIGRATION.md §6.1)
EtherFiRedemptionManager.tokenToRedemptionInfo[EETH/WEETH]defaults to zero after the upgrade.redeemEEth/redeemWeEthrevert untilOPERATION_TIMELOCKcallsinitializeTokenParameters([EETH, WEETH], …). Documented as Day-10+ operational follow-up alongside H7 (per-address rate-limit pre-seeding) and stale-holder hygiene.Misc (deploy.s.sol) — set
logging=falseon LiquidityPool / Liquifier / EtherFiAdmin deploys;Utils.formatStaticParamreverts with "Unsupported static type" onConstructorAddressesstruct args. Deployment still succeeds; only the JSON pretty-print is skipped for these 3.Verification (Tenderly VNet 31698b4b-77c5-4b96-a057-855ccaac67b8)
Upgrade execution
status=0x10x38de2f…ed6f✅0x8e4ea5…1da6f✅0x9cea2f…b364e✅ gas=1,924,7340xbfcce8…bd544✅ gas=1,076,8110xe9b477…64dd0+0x4f38eb…c4b43✅Post-upgrade state verified
EFRM.treasury() = 0x2f5301…F0F(BUYBACK_SAFE — C2 fix verified live) ✓LP.maxWithdrawAmount = 3,000 ETH,LP.minWithdrawAmount = 100k gwei✓EFAdmin.maxFinalizedWithdrawalAmountPerDay≥ 0 (set via Batch 2) ✓LP.pauseUntilDuration = 7 days✓QA scenarios (8 run)
LP.requestWithdraw→ mint NFTBlacklisted(addr)pauseContractUntilvia guardian → deposit reverts withPausedUntil(ts)unPauseContractafterpauseContractUntilPre-broadcast checklist (PLEASE EYEBALL block surfaces all of these)
GIT_COMMIT_SHA(3 places — deploy/transactions/revert.s.sol, must match)HOLDER_*placeholders (SUPER_GUARDIAN, GUARDIAN, ORACLE/HOUSEKEEPING/EXECUTOR/EIGENPOD_OPERATIONS)PAUSE_UNTIL_*durationsADMIN_DAILY_FINALIZED_WITHDRAWAL_LIMITamountPerShareCeil()at deploy dayEFRM.initializeTokenParameters([EETH, WEETH], …)via OPERATING_TIMELOCK (F3)Items deferred / acked
ROLE_MIGRATION.md §4— schedule Batches 1+2 simultaneously, execute 1→2→3 back-to-back.Test plan
forge buildagainst PR Pankaj/feat/security upgrades pr scripts #420 base — clean ✓forge script transactions.s.sol --fork-url $TENDERLY_RPCdry-run end-to-end — all 10 steps green ✓_preflightRoleHashespasses against fresh RR instance ✓verifyDeployedBytecodematches withWITHDRAW_REQUEST_NFT_BUYBACK_SAFEconstructor arg ✓🤖 Generated with Claude Code
Note
High Risk
Changes mainnet upgrade batch ordering, redemption manager wiring, and timelock salts—mistakes could brick upgrades or misconfigure withdrawals/redemptions until follow-up ops run.
Overview
Hardens the 26Q2 security upgrade Forge scripts and runbook after PR #420 review and Tenderly fork QA—no on-chain contract source changes in this diff.
Batch 1 ordering (showstopper fix):
transactions.s.solno longer upgradesRoleRegistryfirst. It runs grants → 21 proxy upgrades + beacon → RR swap → twoonlyUpgradeTimelockinitializers → legacy revokes, because legacy proxy impls still callroleRegistry.onlyProtocolUpgrader, which the new registry drops.Deploy / verify alignment:
EtherFiRedemptionManagertreasury immutables and bytecode checks useWITHDRAW_REQUEST_NFT_BUYBACK_SAFE(not genericTREASURY).ADMIN_STALE_ORACLE_REPORT_BLOCK_WINDOWis7200 * 14. WithdrawRequestNFT share-rate band tightens to0.95–1.15 ether. SharedGIT_COMMIT_SHAdrives CREATE2 and timelock salts (batch-1/batch-2/batch-revert) instead ofblock.number.Preflight & ops visibility:
_printPleaseEyeball, expanded_preflight, and_preflightRoleHashes()run before JSON generation. Deploy skips broken struct logging on three contracts.Batch 2 fork fixes: Existing EtherFiNodesManager rate buckets use
setCapacity/setRefillRateinstead ofcreateNewLimiter._auctionImmSelsdrops removedmembershipManagerContractAddress().Docs:
ROLE_MIGRATION.md§6 adds post-upgrade follow-ups (initializeTokenParameters, per-address rate limits, stale role holders).Reviewed by Cursor Bugbot for commit cb8e5e0. Bugbot is set up for automated code reviews on this repo. Configure here.