Skip to content

Validator consolidation#2758

Open
naddison36 wants to merge 61 commits intomasterfrom
nicka/consolidation
Open

Validator consolidation#2758
naddison36 wants to merge 61 commits intomasterfrom
nicka/consolidation

Conversation

@naddison36
Copy link
Collaborator

@naddison36 naddison36 commented Jan 21, 2026

Overview

A new staking strategy was implemented in 2025 that used the compounding validators introduced in Ethereum’s Pectra release. The plan was to exit from the sweeping (0x01 type) validators used by the old Native Staking Strategy and then deposit to the new compounding (0x02 type) validators used by the new Compounding Staking Strategy.

Unfortunately, since the start of 2026, the validator deposit queue has blown own to over 70 days as at 3 February 2026. As deposit do not earn any yield, having 18.6k ETH (71.1%) of OETH’s collateral not earning yield is not acceptable. A better approach is to use the validator consolidation the was introduced in the Pectra release that reduced the lose of yield to 27 hours.

See https://www.validatorqueue.com/ for deposit queue times.

Objectives/Requirements

  • Migrate from the old sweeping (0x01 type) validators to the new compounding (0x02) validators with minimal yield lose.
  • Keep the contract changes to a minimum given this is a one-off migration.
  • Atomically migrate balances from the old native staking strategy to the new compounding staking strategy. That is, the OETH Vault can not under or over count the amount of ETH across all validators at any point in time.

Link

Consolidation Processes

oethProcesses-consolidation

Contract Changes

New ConsolidationController contract

  • requestConsolidation function
    • parameters sourcePubKeys, targetPubKey
    • only callable by the Origin Admin multi-sig (5/8)
    • check no consolidations are already in progress. ie consolidationCount == 0
    • check target validator is Active on the new Compounding Staking Strategy
    • check no pending deposits in the new target validator. need to iterate over the strategy’s deposits
    • Store
      • number of validators being consolidated
      • the timestamp of the consolidation request
      • the source strategy
      • target validator hash
    • call requestConsolidation on the old Native Staking Strategy
    • call snapBalances on the new Compounding Staking Strategy as this will be the last time the balance can be updated until after the consolidation process has completed.
  • confirmConsolidation function
    • parameters balanceProofs and pendingDepositProofs
    • only callable by the Origin Admin multi-sig (5/8)
    • check a consolidation is in progress. ie consolidationCount > 0
    • check the minimum time the consolidation could be processed has passed
    • reset the consolidation state
    • call verifyBalances on the new Compounding Staking Strategy with the balance and pending deposit proofs
    • call confirmConsolidation on the old Native Staking Strategy with the number of validators that were consolidated.
  • failConfirmation function
    • parameters sourcePubKeys
    • only callable by the Origin Admin multi-sig (5/8)
    • check a consolidation is in progress. ie consolidationCount > 0
    • Reduce the number of validators being consolidated
    • call failConsolidation on the old Native Staking Strategy
  • The following functions are only callable by the Validator Registrator and forward to the old Native Staking Strategy
    • doAccounting - can be forwarded at any time
    • exitSsvValidator - can be forwarded at any time
    • removeSsvValidator - can only be forwarded if no consolidation is in progress
  • The following functions are only callable by the Validator Registrator and forward to the Compounding Staking Strategy
    • validatorWithdrawal - full validator exits are prevented. Partial withdrawals are allowed
    • stakeEth - deposits to a target validator of an active consolidation is prevented. Initial 1 ETH deposits to new validators and top-ups are allowed.
  • verifyBalances can be called by anyone and forward to the the Compounding Staking Strategy when
    • there is no consolidation in progress
    • the balances snapshot time is the consolidation start time

Changes to the old Native Staking Strategy

  • new requestConsolidation function
    • parameters sourcePubKeys and targetPubKey
    • is only callable by the validator registrator that is changed to new ConsolidationController contract
    • for each source validator
  • new confirmConsolidation function
    • parameter is the number of validators that were consolidated
    • is only callable by the validator registrator that is changed to new ConsolidationController contract
    • decrease the activeDepositedValidators by the number of validators that were consolidated. This effectively decreases the strategy’s balance from checkBalance by 32 ETH times thye number of validators that were consolidated.
  • new failConsolidation function
    • parameters sourcePubKeys
    • is only callable by the validator registrator that is changed to new ConsolidationController contract
    • for each source validator
      • check the validator is EXITING
      • store the validator as STAKED

Security Audit Scope

In scope contracts

  • ConsolidationController
  • NativeStakingSSVStrategy with a focus on the requestConsolidation, failConsolidation and confirmConsolidation functions
  • CompoundingStakingSSVStrategy with a focus on snapBalances and verifyBalances

Out of scope contracts

  • FeeAccumulator
  • BeaconProofs
  • CompoundingStakingStrategyView

Tests

Consolidation fork tests are in contracts/test/strategies/stakingConsolidation.mainnet.fork-test.js

# Start a local node
pnpm run node

# Run the fork tests
pnpm run test:fork

Deployment

Deployment script is in contracts/deploy/mainnet/173_staking_consolidation.js

pnpm run deploy:mainnet

Code Change Checklist

To be completed before internal review begins:

  • The contract code is complete
  • Executable deployment file
  • Fork tests that test after the deployment file runs
  • Unit tests *if needed
  • The owner has done a full checklist review of the code + tests

Internal review:

  • Two approvals by internal reviewers

@codecov
Copy link

codecov bot commented Jan 21, 2026

Codecov Report

❌ Patch coverage is 60.00000% with 50 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.64%. Comparing base (ccb5abc) to head (0185f07).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
...rategies/NativeStaking/ConsolidationController.sol 57.14% 39 Missing ⚠️
.../strategies/NativeStaking/ValidatorRegistrator.sol 62.06% 11 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2758      +/-   ##
==========================================
+ Coverage   39.02%   39.64%   +0.62%     
==========================================
  Files         126      127       +1     
  Lines        5825     5948     +123     
  Branches     1546     1587      +41     
==========================================
+ Hits         2273     2358      +85     
- Misses       3550     3588      +38     
  Partials        2        2              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@sparrowDom sparrowDom left a comment

Choose a reason for hiding this comment

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

Did a part review. Left some comments proposing how we could speed up consolidations.

}(sourcePubKeys, targetPubKey);

// Snap the balances for the last time on the new Compounding Staking Strategy
// until the consolidations are confirmed
Copy link
Member

Choose a reason for hiding this comment

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

nit: outdated comment. Balances can be snapped even after the consolidation has initiated. By anyone.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

any can call snapBalances, but those snaps can't be verified with verifyBalances when a consolidation is in progress. The owner needs to call confirmConsolidation which verifies the balances against a new snapped balance.

Copy link
Member

Choose a reason for hiding this comment

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

I guess I am a little confused of the purpose of balance snapping at the time the consolidations initiate. Since 35 slots after consolidations begin (SNAP_BALANCES_DELAY) anyone could call snapBalances and invalidate the possibility of validating the balances after the time the consolidations initiate (in case our off-chain component isn't fast enough with proof generation).
Just a note here: there is also a possibility that the previous slot hasn't proposed a block. There is a small chance of that occurring, but it could make it impossible to verify the balances against the block snapped at the time of consolidation.

Is the goal of balances snapping at the time of consolidation to make sure that the state of accounting isn't too stale before the consolidations begin?

/**
* @notice A consolidation request can fail to be processed on the beacon chain
* for various reasons. For example, the pending consolidation queue is full with 262,144 requests.
* Or the source validator has exited from a voluntary exit request.
Copy link
Member

Choose a reason for hiding this comment

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

Or even more likely the churn limit has been reached and the current epoch is ignoring the requests.

*/
function requestConsolidation(
address _sourceStrategy,
bytes[] calldata sourcePubKeys,
Copy link
Member

Choose a reason for hiding this comment

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

Current churn limit for consolidations per epoch is Ξ304. Meaning that if no one else is requesting consolidations we will at most get 9 validators into the consolidation process. I would add a require where the transaction reverts if sourcePubKeys.length > 9

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Where does this Ξ304 limit come from?

Copy link
Member

Choose a reason for hiding this comment

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

Explained by Grok:
The 304 ETH/epoch consolidation churn limit is derived from the base churn amount minus the capped activation/exit churn limit, calculated as follows (all values in Gwei for precision, with ETH equivalents for clarity):

  • Retrieve the total active effective balance on mainnet (~36.74 million ETH, or ~36.74e15 Gwei).
  • Compute the base churn as total_active_balance // CHURN_LIMIT_QUOTIENT, where CHURN_LIMIT_QUOTIENT = 65,536 (~561 ETH equivalent).
  • The activation/exit churn limit is capped at MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT = 256 ETH (or 256e9 Gwei).
  • The consolidation churn limit is then the base churn minus this capped activation/exit limit (~561 - 256 = 305 ETH, but precisely 304 ETH after Gwei-level floor division and minimum checks against MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA = 128 ETH).

bytes calldata targetPubKey
) external payable onlyOwner {
// Check no consolidations are already in progress
require(consolidationCount == 0, "Consolidation in progress");
Copy link
Member

Choose a reason for hiding this comment

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

After some thought I think we could do with an alternate approach. Currently the contract is designed to support consolidation process in the following manner:

  • up to 9 validators can be put into the consolidation queue per transaction (actually more validators are accepted by the contract, but beacon chain will ignore those requests and the consolidations will need to be failed)
  • then the contract needs to wait for 27 hours (or more) for the consolidations to finalize
  • another batch of 9 validators can be initiated to be consolidated.

What it we modify the behaviour where we allow:

  • multiple kick-offs of the 9 validator consolidations (which we can trigger every epoch)
  • where we:
    • add new ones to the sum of: consolidationCount
    • leave the consolidationStartTimestamp to the one of the first request
    • impose a restriction where sourceStrategy and targetPubKeyHash of the multiple requests must match
    • set a hard limit of how many validators can be added in multiple requests before the consolidation is finished / balances verified

Copy link
Member

@sparrowDom sparrowDom left a comment

Choose a reason for hiding this comment

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

Have completed the review, left some comments inline.

Happy to re-review it once / if we do the changes where more than 9 validators can enter the consolidation process.

@sparrowDom
Copy link
Member

Also very good job on the fork tests. Nice to see such thorough coverage.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants