26Q2 - Security Upgrades#385
Conversation
📊 Forge Coverage ReportGenerated by workflow run #775 |
|
|
||
| import "../interfaces/IRoleRegistry.sol"; | ||
|
|
||
| contract PausableUntil { |
There was a problem hiding this comment.
Does it make sense to make this abstract? All functions internal and never to be deployed ?
There was a problem hiding this comment.
fine to make it abstract
| import "./utils/PausableUntil.sol"; | ||
|
|
||
| contract EtherFiRateLimiter is IEtherFiRateLimiter, Initializable, UUPSUpgradeable, PausableUpgradeable { | ||
| contract EtherFiRateLimiter is IEtherFiRateLimiter, Initializable, UUPSUpgradeable, PausableUpgradeable, PausableUntil { |
There was a problem hiding this comment.
What advantage do we have of adding pausable on Ratelimiter ?
| function _transferShares(address _sender, address _recipient, uint256 _sharesAmount) internal whenNotPaused { | ||
| blacklister.nonBlacklisted(_sender); | ||
| blacklister.nonBlacklisted(_recipient); |
There was a problem hiding this comment.
Need to add this for safe guard from transferFrom similar to MembershipNFT checks:
blacklister.nonBlacklisted(msg.sender);
| } | ||
| } | ||
|
|
||
| uint256 public constant MAX_PAUSE_DURATION = 7 days; |
There was a problem hiding this comment.
We would be reverting back to 1 day?
| import "./interfaces/IRoleRegistry.sol"; | ||
| import "./interfaces/IBlacklister.sol"; | ||
|
|
||
| contract WeETH is ERC20Upgradeable, UUPSUpgradeable, OwnableUpgradeable, ERC20PermitUpgradeable, IRateProvider, AssetRecovery { |
There was a problem hiding this comment.
Why haven't we added PausableUntil to weETH ? @0xpanicError
There was a problem hiding this comment.
we discussed that pausable until shouldn't be applies on token transfer. so was only added on mint/burn on eeth. not any ops on weETH.
There was a problem hiding this comment.
That is not exact description of our discussion
https://discord.com/channels/827197391068856371/1501877625784893490/1504007820696354899
Unless we have high confidence on having only 'blacklistUntil', we should add 'pausableUntill'.
There was a problem hiding this comment.
Review this Pausing/Unpausing functions. Either make them complete across most contracts, or remove this from here.
Cannot have inconsistency
| if (_report.protocolFees < 0) return (false, "EtherFiAdmin: protocol fees can't be negative"); | ||
| int128 totalRewards = _report.protocolFees + _report.accruedRewards; | ||
| // protocol fees are less than 20% of total rewards | ||
| if (_report.protocolFees > 0 && _report.protocolFees * 5 > totalRewards) return (false, "EtherFiAdmin: protocol fees exceed 20% total rewards"); |
There was a problem hiding this comment.
Would be best to use a defined constant instead of 5
There was a problem hiding this comment.
We need to add per node address sweeping function
There was a problem hiding this comment.
Need to get rid of this EtherFIAdmin setter
| function redeemEEth(uint256 eEthAmount, address receiver, address outputToken) public whenNotPaused nonReentrant nonBlacklisted(receiver) { | ||
| _redeemEEth(eEthAmount, receiver, outputToken); | ||
| } |
There was a problem hiding this comment.
Do we want to check if the sender is blacklisted or not for all functions here?
There was a problem hiding this comment.
shuoldn't it be both caller of the function and receiver of the fund which need to be checked?
There was a problem hiding this comment.
the modifier checks both.
| uint128 public accumulatedRevenueThreshold; | ||
|
|
||
| mapping(address => bool) public admins; | ||
| mapping(address => bool) public DEPRECATED_admins; |
There was a problem hiding this comment.
let's make the deprecated variable, private
|
|
||
| function initializeOnUpgrade(address _membershipManagerContractAddress, uint128 _accumulatedRevenueThreshold, address _etherFiAdminContractAddress, address _nodeOperatorManagerAddress) external onlyOwner { | ||
| require(_membershipManagerContractAddress != address(0) && _etherFiAdminContractAddress != address(0) && _nodeOperatorManagerAddress != address(0), "No Zero Addresses"); | ||
| function initializeOnUpgrade(address _membershipManagerContractAddress, uint128 _accumulatedRevenueThreshold, address _nodeOperatorManagerAddress) external onlyOwner { |
There was a problem hiding this comment.
remove this function, if not needed anymore
…rded-call-whitelist Re-grant ENM forwarded-call whitelist to eigenpod-ops role in Batch 2
…-finalize-atomicity test: atomic I3 finalize+lock (Bugbot #470 "Lock finalize not rolled back")
…grades-PR-scripts Pankaj/feat/security upgrades pr scripts
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 84c79e1. Configure here.
| } | ||
| verified++; | ||
| } | ||
| console.log("Post-migration verification passed for ids:", verified); |
There was a problem hiding this comment.
Migration verify runs after broadcast
Medium Severity
The V0 migration script broadcasts all batch transactions inside vm.startBroadcast / vm.stopBroadcast, then runs post-migration checks that can revert with MigrationIncomplete. A failed final verification still leaves earlier batches on-chain while the script run reports failure.
Reviewed by Cursor Bugbot for commit 84c79e1. Configure here.
…nto seongyun/fuzz/security-upgrades
…th + upgrade gate The conf files pointed at pre-restructure paths (src/LiquidityPool.sol, src/EETH.sol, src/RoleRegistry.sol) and the I8 filter referenced the deleted returnLockedEth(uint128), so certoraRun could not execute against this tree and the previously cited reports attested to older contract revisions. - Fix file paths to src/core/ and src/governance/; refresh package maps and add the EETH:liquidityPool back-link. - I8: add batchCreateBeaconValidators (new guarded ETH-outflow path) and receive() (via f.isFallback) to the filter; drop returnLockedEth. - rule_sanity basic everywhere; the tautological I9 lemma moves to its own LiquidityPoolDecomposition conf with sanity none so vacuity checking stays on for I7/I8. - RoleRegistry: restore the grant-path proof (successful grantRole/ revokeRole/setRole implies msg.sender == owner()) via a scoped unresolved-call DISPATCH for solady's owner()/MAX_ROLE() self-staticcalls (sound: both target address(this)); add the upgradeToAndCall gate rule (success implies caller held UPGRADE_TIMELOCK_ROLE). - Reframe I7 comments honestly as guard-wiring regression protection; refresh all stale line references. All three configs re-verified at this head, No errors found by Prover, sanity basic passing: - LiquidityPoolPeg (I7/I8): report d9f2667b30f04c54b77f70e5080afa0a - LiquidityPoolDecomposition (I9): report acdc5726dc6143afaed9244e921f68a2 - RoleRegistryAuthority (I6.1-I6.6): report c2be9a4b97454e7ba9a3cb5b06a347ba Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… review Each fix targets a case where a buggy implementation would previously have passed the suite green. Validator suites: - doCreate sent 1 ether to the non-payable batchCreateBeaconValidators, so every call reverted and the REGISTERED->CONFIRMED edge was never tested; the combined create+invalidate vacuity gate hid it. Call is now valueless (the pool self-funds), the gate is split per-edge, and a one-shot coverage-floor bootstrap guarantees both terminals fire every run. - Successful transitions now assert their exact end state, not just any legal edge; illegal edges record the observed before/after pair. - linkLegacyValidatorIds (the second writer to etherFiNodeFromPubkeyHash) is now exercised with an already-linked pubkey and must revert AlreadyLinked; relink attacks use fresh legacy ids (1000+) so they isolate the pubkey-hash guard; I11 gains a global first-seen sweep. - Fork pinned at block 25447657. RateLimiter: - Refill asserted against the exact model min(capacity, before + rate*dt) instead of monotonic+bounded only; occasional near-uint64-max capacity/ rate and long warps reach the clamp regions. - Draining exactly consumable(id) must succeed (was a swallowed revert); consumer revocation/re-grant round-trip added; admin setters assert their exact documented post-state and must not revert on valid input; consume_ok counts only amount>0; depth 128; per-probe coverage gates with a deterministic coverage-floor bootstrap. Oracle: - Duplicate submission by one member must revert ReportNotNeeded and never reach quorum; conflicting same-range reports must not reach consensus. - Freshness probed at exactly wait-1 (reject) and wait (apply); APR probed at the exact boundary reward and one wei above; negative rebase gets a valid small apply plus a one-wei-over-3bps reject (new numRejNegRebase category); setUp pins maxNegativeRebaseBps to the production 3. - ghost_everStuck fails the run if the wedge guard eats a sequence tail. RewardsDistributor: - I13 delay predicate now computed from ghost state (setAt + tracked delay), not the contract's own storage, so write-side timestamp corruption is caught; deterministic boundary probe at delay-1/delay. - Real 4-leaf Merkle tree with genuine 2-element proofs (matching _verifyAsm's sorted-pair hashing) plus a tampered-amount reject, so the proof-verification loop finally executes. WithdrawalSolvency: - Fork pinned at 25447657; setUp derives backlog/deposit sizing from chain state instead of hardcoded live-state assumptions; the silent finalize break is now a loud require. - Claims assert exact deltas: recipient balance += payout, lock -= amountOfEEth, totalValueOutOfLp -= payout + stranded sweep. - invalidate/validate lifecycle probes incl. the finalized-invalidate rejection; P2 relabeled assumption-scoped (bounded rebases) instead of PROVED; vacuity gates baseline-aware against the seeded lifecycle. All six suites green: 36 invariant functions, fork suites at the pinned block, non-fork suites across 5 seeds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The workflow only triggered on PRs into staging-2.5 and master, so the invariant layer merging into pankaj/feat/security-upgrades* never executed in CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… into seongyun/fuzz/security-upgrades # Conflicts: # test/invariant/WithdrawalSolvency.invariant.t.sol # test/invariant/handlers/ValidatorStateMachineHandler.sol
…rebase cap The generic valid-apply scenarios sized accruedRewards from the APR-cap boundary only (_maxSafeReward / 2). The APR gate scales with the report range's elapsed time, so over a long range that bound can exceed LiquidityPool's absolute MAX_POSITIVE_REBASE_BPS cap: the report passes EtherFiAdmin validation, reverts inside LiquidityPool.rebase with RebaseExceedsPositiveCap, and stays published-but-unhandled — wedging the oracle for the rest of the sequence. _maxSafeReward now returns min(APR boundary, LP positive cap), covering all three apply sites; the APR-boundary scenario keeps its own exact unclamped bound with its existing lpCap guard. Found by Cursor Bugbot on 893bd9a. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setUp reverted at fork creation in CI (both runs on this PR, deterministic, gas 0) while passing locally against an archive node: CI's MAINNET_RPC_URL provider does not serve state for the old pin 25383537 (~64k blocks deep). Re-pin to 25447657, the same block the invariant fork suites use, which the same CI runs served successfully. The test self-seeds its pre-state, so any block predating the on-chain security upgrade is equivalent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ork pin Root cause of the ForwardedCallWhitelistRegrant setUp failure in CI: the workflow installs foundry 'nightly', and current nightlies enforce the EIP-3860 initcode-size limit inside tests. WhitelistHarness inherits the full SecurityUpgradesScript (~542 KB initcode, limit 48 KB), so 'new WhitelistHarness()' reverts and setUp dies with a bare EvmError at gas 0 — while the same test passes on stable, which does not enforce the limit in tests. Verified by isolating harness deployment on forge 1.7.2-nightly (fails) vs 1.5.1-stable (passes); fork creation at either pinned block works on both, so the earlier block re-pin (1019043) was a misdiagnosis and is reverted here. Pinning CI to the stable release used locally fixes the failure and stops nightly regressions from breaking CI unpredictably. Bump deliberately when upgrading toolchains. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…upgrades test: invariant fuzz + Certora CVL coverage for protocol invariants (I4,I6-I13)


Note
Medium Risk
Touches governance/liquidity formal specs and many deployment/upgrade scripts aligned with role consolidation and adapter/oracle shape changes; on-chain impact depends on the separate contract upgrade batch, but wrong script constants or bytecode gates could block or mis-verify releases.
Overview
This PR bundles release and verification tooling for the 26Q2 security-upgrades line, alongside broad script/import refactors that track the consolidated role model and new contract layout.
Formal verification: New Certora configs and CVL specs prove LiquidityPool peg/solvency wiring (I7
nonDecreasingRateon guarded entry points, I8 LP-buffer solvency after guarded writes, I9 pooled-ether decomposition in a separate tautology spec) and RoleRegistry authority (I6 owner/revokeAdmin/upgrade gates, no stray role mutation).CI & toolchain: Forge tests on PRs targeting
pankaj/feat/security-upgrades*; Foundry pinned to v1.5.1 (avoids nightly EIP-3860 initcode failures on large harnesses);OP_RPC_URLadded to the test env;foundry.lockgitignored.Build layout:
remappings.txtadds@etherfi/,@tests/,@scripts/,@eigenlayer-libraries/; scripts and libs switch from../src/...to those aliases. Eigenlayer library imports use the remapped package path.Deployment safety:
ContractCodeCheckergainsassertByteCodeMatch— a reverting bytecode equality check that masks only address-derived immutables (and optional EIP-712 domain words), unlike the logging-only verifier.Scripts & ops: Timelock delay constants renamed (
minDelay_*); role checks use consolidatedRoleRegistrytier roles instead of per-contract role constants;EtherFiNodedeploys droproleRegistry;EtherFiRateLimitertakes mainnet eETH/weETH immutables;WeETHWithdrawAdapterdropswithdrawRequestNFTfrom ctor/deploy verification;OracleReportconstruction dropswithdrawalRequestsToInvalidate. Newdeployment/*JSON records Create2 deployment parameters. Added V0→V1 membership batch migration (MembershipV0Migrator,MigrateV0ToV1.s.sol, id lists). RemovedSimulateBatchApprove.s.sol.Reviewed by Cursor Bugbot for commit f9a8cf5. Bugbot is set up for automated code reviews on this repo. Configure here.
Timelock batch simulations (Batch 1 — UPGRADE_TIMELOCK, 10-day delay)
Tenderly public simulations of the two proposed Safe transactions (overriding the existing queued txns, nonces 177 / 178):
UPGRADE_TIMELOCK.scheduleBatch: https://dashboard.tenderly.co/public/simulator/08c1f4d6-da39-478a-8d66-ab8de4323a17UPGRADE_TIMELOCK.executeBatch, now includes the L1SyncPool + AvsOperatorManager upgrades; simulated with the scheduled operation marked ready (i.e. schedule executed + 10-day delay elapsed) so it passes: https://dashboard.tenderly.co/public/simulator/f56b0565-2357-4169-890e-5e496a68617f — ✅ success, gasUsed 2,489,052.