Network Balances Process Specification¶
Version¶
v1.0 - Initial specification
Overview¶
The network balances process calculates and submits the total user ETH provided by rETH holders, aggregating balances from the deposit pool, minipools, megapools, fee distributors, smoothing pool, and rETH contracts at regular intervals.
Technical Prerequisites¶
- Integer Arithmetic: All calculations use big integer arithmetic to avoid floating-point precision errors
- On-chain State: Process operates on finalized beacon chain epochs to ensure data stability
- Oracle DAO Membership: Only Oracle DAO members can submit network balances
- Submission Intervals: Submissions occur at configured intervals (default: configurable via protocol settings)
- Deterministic Calculations: All calculations must be deterministic across Oracle DAO members, with state snapshots pinned to identical blocks to ensure consistency
- Consensus Mechanism: Multiple Oracle DAO members submit identical calculations, with protocol consensus determining the accepted values
Scheduling and Target Block Selection¶
Submission Timing Calculation¶
The target submission time is calculated using a reference timestamp and configured interval:
Where:
| Term | Definition |
|---|---|
reference_timestamp |
Off-chain configuration parameter (not on-chain) |
submission_interval_seconds |
rocketDAOProtocolSettingsNetwork.getSubmitBalancesFrequency() |
interval_count |
Number of complete intervals since reference timestamp |
Target Block Selection¶
The process determines the target execution layer block using the submission timestamp:
-
Calculate target beacon slot:
-
Find the latest execution block at or before the target slot (searching back up to 32 slots if necessary)
-
Verify the target epoch is finalised before proceeding
Network Balance Components¶
The process calculates the following balance components:
1. Deposit Pool Balance¶
User deposits awaiting minipool assignment.
2. Minipool Balances¶
For each minipool, calculate user share based on status:
Dissolved Minipools¶
Status: minipool.status == Dissolved
Initialised/Prelaunch Minipools¶
Status: minipool.status == Initialized || minipool.status == Prelaunch
Special Case: Redstone LEB v2 ("Broken" LEBs)¶
Status: minipool.version == 2 && minipool.getDepositType() == Variable
This case is evaluated before the standard active path, including the inactive validator fallback below.
user_balance = minipool_balance + beacon_balance - node_refund_balance - node_deposit_balance
is_staking = (validator_exists && activation_epoch < current_epoch && exit_epoch > current_epoch)
Where:
| Term | Definition |
|---|---|
beacon_balance |
Beacon Chain API /eth/v1/beacon/states/{stateId}/validator_balances |
minipool_balance |
ETH balance of minipool |
node_refund_balance |
minipool.getNodeRefundBalance() |
node_deposit_balance |
minipool.getNodeDepositBalance() |
validator_exists |
Beacon Chain API /eth/v1/beacon/states/{stateId}/validators (true if validator data present in response) |
activation_epoch, exit_epoch |
Beacon Chain API /eth/v1/beacon/states/{stateId}/validators |
current_epoch |
Calculated as beacon_slot / slots_per_epoch |
Active Minipools (Standard)¶
Status: minipool.status == Staking
Inactive Validator Fallback¶
If the validator does not yet exist on the beacon chain, or has not yet activated (activation_epoch >= current_epoch), fall back to the deposit balance:
Active Validator¶
total_balance = beacon_balance + minipool_contract_balance - node_refund_balance
user_balance = minipool.calculateUserShare(total_balance)
is_staking = (exit_epoch > current_epoch)
Where:
| Term | Definition |
|---|---|
beacon_balance |
Beacon Chain API /eth/v1/beacon/states/{stateId}/validator_balances |
minipool_contract_balance |
ETH balance of minipool |
node_refund_balance |
minipool.getNodeRefundBalance() |
activation_epoch, exit_epoch |
Beacon Chain API /eth/v1/beacon/states/{stateId}/validators |
current_epoch |
Calculated as beacon_slot / slots_per_epoch |
slots_per_epoch |
Beacon Chain API /eth/v1/config/spec field SLOTS_PER_EPOCH |
Special Case: Full Minipools in Refund Queue¶
Status: minipool.getDepositType() == Full && minipool.getUserDepositBalance() == 0
user_balance = minipool.calculateUserShare(total_balance) - 16 ETH
is_staking = (exit_epoch > current_epoch)
| Term | Definition |
|---|---|
beacon_balance |
Beacon Chain API /eth/v1/beacon/states/{stateId}/validator_balances |
minipool_contract_balance |
ETH balance of minipool |
node_refund_balance |
minipool.getNodeRefundBalance() |
exit_epoch |
Beacon Chain API /eth/v1/beacon/states/{stateId}/validators |
current_epoch |
Calculated as beacon_slot / slots_per_epoch |
slots_per_epoch |
Beacon Chain API /eth/v1/config/spec field SLOTS_PER_EPOCH |
Aggregated Results:
minipool_total_balance = Σ(user_balance) for all minipools
minipool_staking_balance = Σ(user_balance) for minipools where is_staking == true
3. Megapool Balances¶
For each megapool:
- Calculate Megapool Beacon Balance:
Each megapool validator's beacon balance contribution is determined using smart contract state rather than raw beacon chain balance alone. This is necessary because validators pass through transitory states — such as prestake, pending activation, and exiting — where funds are moving between the execution layer and consensus layer. Using on-chain state from rocketMegapoolManager.getValidatorInfo(index) alongside beacon chain data ensures balance is accounted for continuously and accurately as it transitions between layers.
Validator State Machine:
| Validator State | Beacon Balance Contribution | Staking Balance Contribution |
|---|---|---|
| Dissolved / Exited / InQueue | Excluded entirely | None |
| InPrestake | LastRequestedValue (milli-ETH, converted to wei) |
None |
| Exiting, beacon balance == 0 | Withdrawal amount recovered from beacon chain withdrawal scan | None |
| Exiting, beacon balance > 0 | validator_balance (gwei, converted to wei) |
validator_balance - reduced_bond |
Staked, not yet activated (activation_epoch == FAR_FUTURE_EPOCH or activation_epoch > current_epoch) |
DepositValue (milli-ETH, converted to wei) |
None |
Staked, active (activation_epoch < current_epoch && exit_epoch > current_epoch) |
validator_balance (gwei, converted to wei) |
validator_balance - reduced_bond |
Where:
| Term | Definition |
|---|---|
validator_balance |
Beacon Chain API /eth/v1/beacon/states/{stateId}/validator_balances |
activation_epoch, exit_epoch, withdrawable_epoch |
Beacon Chain API /eth/v1/beacon/states/{stateId}/validators |
current_epoch |
Calculated as beacon_slot / slots_per_epoch |
reduced_bond |
rocketDAOProtocolSettingsNode.getReducedBond() (protocol-governed parameter, currently 4 ETH) |
LastRequestedValue, DepositValue |
On-chain megapool validator info fields (stored in milli-ETH) |
| Withdrawal scan | For exiting validators with zero beacon balance: search from the validator's withdrawable_epoch on the beacon chain to find the withdrawal record and recover the withdrawn amount |
Aggregated Results:
megapool_beacon_total = Σ(beacon_balance_contribution) for all megapool validators
megapool_staking_balance = Σ(staking_balance_contribution) for all megapool validators
total_withdrawn_balance = Σ(withdrawn_amount) for exiting validators with zero beacon balance
- Calculate User Share:
The reward calculation separates beacon-side balance growth from contract-side pending rewards:
pending_rewards = megapool.getPendingRewards() - total_withdrawn_balance
beacon_balance_increase = megapool_beacon_total - user_capital - node_bond
rewards = beacon_balance_increase + pending_rewards
The reward split is then calculated based on the sign of rewards:
if rewards > 0:
reward_split = megapool.calculateRewards(rewards)
reth_rewards = reward_split.reth_rewards
else if rewards < 0 and (rewards + node_bond) < 0:
// Losses exceed node bond — excess loss attributed to rETH holders
reth_rewards = rewards + node_bond // (negative value)
else:
reth_rewards = 0
user_share = user_capital + reth_rewards
Where:
| Term | Definition |
|---|---|
megapool_beacon_total |
Sum of beacon balance contributions from step 1 |
total_withdrawn_balance |
Sum of recovered withdrawal amounts for exiting validators with zero beacon balance (from step 1) |
user_capital |
megapool.getUserCapital() |
node_bond |
megapool.getNodeBond() |
pending_rewards |
megapool.getPendingRewards() minus total_withdrawn_balance (to avoid double-counting funds already accounted for in the beacon total) |
calculateRewards |
Must be called with state pinned to the target execution layer block number for determinism |
Note on Negative Rewards (Loss Sharing): In normal slashing scenarios, the node operator's bond is sufficient to absorb the penalty and rETH holders are unaffected. Excess loss reaching rETH holders would only occur in a network-wide correlated slashing event — an extremely unlikely scenario. Even then, the node bond provides a buffer, and only losses beyond that bond are reflected as a negative reth_rewards value, reducing the megapool's user share below user_capital.
Aggregated Results:
megapool_user_share_total = Σ(user_share) for all megapools
megapool_staking_balance = Σ(staking_balance_contribution) for all active megapool validators
4. Fee Distributor Balances¶
For each registered node, resolve its fee distributor address and sum the user share of its balance:
for i in 0..rocketNodeManager.getNodeCount():
nodeAddress = rocketNodeManager.getNodeAt(i)
distributor_address = rocketNodeDistributorFactory.getProxyAddress(nodeAddress)
distributor_share_total += distributor.getUserShare()
5. Smoothing Pool Share¶
Estimated portion of smoothing pool balance attributable to rETH holders using rewards tree generation logic.
6. rETH Contract Balance¶
ETH held directly in the rETH token contract.
7. rETH Supply¶
Total rETH tokens in circulation.
8. Node Credit Balance¶
Total deposit credits owed to node operators.
for i in 0..rocketNodeManager.getNodeCount():
nodeAddress = rocketNodeManager.getNodeAt(i)
node_credit_balance += rocketNodeDeposit.getNodeDepositCredit(nodeAddress)
Final Calculations¶
Total ETH Calculation¶
total_eth = deposit_pool_balance
+ minipool_total_balance
+ megapool_user_share_total
+ distributor_share_total
+ smoothing_pool_share
+ reth_contract_balance
- node_credit_balance
Exchange Rate Calculation¶
Where:
| Term | Definition |
|---|---|
reth_supply |
rocketTokenRETH.totalSupply() |
Exchange Rate Clamping (Saturn)¶
When Saturn is deployed, the exchange rate change is clamped to a governance-set maximum to prevent sudden rate spikes or drops (e.g. from oracle errors or manipulation):
max_reth_delta = rocketDAOProtocolSettingsNetwork.getMaxRethDelta()
last_submission_rate = rocketNetworkBalances.getRETHExchangeRate()
rate_change = exchange_rate - last_submission_rate
if rate_change > max_reth_delta:
clamped_rate = last_submission_rate + max_reth_delta
submitted_total_eth = total_eth * clamped_rate / exchange_rate
else if rate_change < -max_reth_delta:
clamped_rate = last_submission_rate - max_reth_delta
submitted_total_eth = total_eth * clamped_rate / exchange_rate
else:
submitted_total_eth = total_eth
The value actually submitted on-chain is submitted_total_eth, not the raw total_eth.
Total Staking ETH Calculation¶
Staking ETH Guard¶
After clamping, the staking balance cannot exceed the submitted total ETH:
Submission Process¶
Data Validation¶
- Check if identical balances were previously submitted for the target block
- Verify submission hasn't occurred too recently based on configured interval
Smart Contract Submission¶
The process calls the rocketNetworkBalances.submitBalances() function with:
- block: Target execution block number
- slotTimestamp: Calculated submission timestamp
- totalEth: Clamped total ETH value (submitted_total_eth — may differ from raw total_eth if rate clamping was applied)
- stakingEth: Total staking ETH value (after staking guard applied)
- rethSupply: Current rETH token supply