From a58c2232f544a29fc031a3f4544d96b0f439532d Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Tue, 20 Jan 2026 21:07:30 -0300 Subject: [PATCH 1/4] Distribution code was considering just SP balance being distributed and ignoring the megapool pending rewards --- bindings/utils/state/network.go | 4 +- shared/services/rewards/generator-impl-v11.go | 38 ++++++++++--------- shared/services/rewards/mock_v11_test.go | 2 +- shared/services/rewards/test/mock.go | 2 +- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/bindings/utils/state/network.go b/bindings/utils/state/network.go index 3f48fb835..720ae7da5 100644 --- a/bindings/utils/state/network.go +++ b/bindings/utils/state/network.go @@ -82,7 +82,7 @@ type NetworkDetails struct { // Saturn MegapoolRevenueSplitSettings MegapoolRevenueSplitSettings MegapoolRevenueSplitTimeWeightedAverages MegapoolRevenueSplitTimeWeightedAverages - SmoothingPoolPendingVoterShare *big.Int `json:"smoothing_pool_earmarked_voter_share_eth"` + PendingVoterShareEth *big.Int `json:"pending_voter_share_eth"` } // Create a snapshot of all of the network's details @@ -170,7 +170,7 @@ func NewNetworkDetails(rp *rocketpool.RocketPool, contracts *NetworkContracts, i contracts.Multicaller.AddCall(contracts.RocketNetworkRevenues, &details.MegapoolRevenueSplitTimeWeightedAverages.NodeShare, "getCurrentNodeShare") contracts.Multicaller.AddCall(contracts.RocketNetworkRevenues, &details.MegapoolRevenueSplitTimeWeightedAverages.VoterShare, "getCurrentVoterShare") contracts.Multicaller.AddCall(contracts.RocketNetworkRevenues, &details.MegapoolRevenueSplitTimeWeightedAverages.PdaoShare, "getCurrentProtocolDAOShare") - contracts.Multicaller.AddCall(contracts.RocketRewardsPool, &details.SmoothingPoolPendingVoterShare, "getPendingVoterShare") + contracts.Multicaller.AddCall(contracts.RocketRewardsPool, &details.PendingVoterShareEth, "getPendingVoterShare") } _, err := contracts.Multicaller.FlexibleCall(true, opts) if err != nil { diff --git a/shared/services/rewards/generator-impl-v11.go b/shared/services/rewards/generator-impl-v11.go index 48a1ae491..4b16e3c83 100644 --- a/shared/services/rewards/generator-impl-v11.go +++ b/shared/services/rewards/generator-impl-v11.go @@ -491,7 +491,7 @@ func (r *treeGeneratorImpl_v11) calculateEthRewards(checkBeaconPerformance bool) // Get the Smoothing Pool contract's balance r.smoothingPoolBalance = r.networkState.NetworkDetails.SmoothingPoolBalance r.log.Printlnf("%s Smoothing Pool Balance:\t%s\t(%.3f)", r.logPrefix, r.smoothingPoolBalance.String(), eth.WeiToEth(r.smoothingPoolBalance)) - r.log.Printlnf("%s Earmarked Voter Share:\t%s\t(%.3f)", r.logPrefix, r.networkState.NetworkDetails.SmoothingPoolPendingVoterShare.String(), eth.WeiToEth(r.networkState.NetworkDetails.SmoothingPoolPendingVoterShare)) + r.log.Printlnf("%s Voter Share from Megapools:\t%s\t(%.3f)", r.logPrefix, r.networkState.NetworkDetails.PendingVoterShareEth.String(), eth.WeiToEth(r.networkState.NetworkDetails.PendingVoterShareEth)) if r.rewardsFile.Index == 0 { // This is the first interval, Smoothing Pool rewards are ignored on the first interval since it doesn't have a discrete start time @@ -850,7 +850,8 @@ func (r *treeGeneratorImpl_v11) calculateNodeRewards() (*nodeRewards, error) { var err error bonusScalar := big.NewInt(0).Set(oneEth) - voterEth := big.NewInt(0) + voterEthFromSmoothingPool := big.NewInt(0) + totalVoterEth := big.NewInt(0) pdaoEth := big.NewInt(0) // If pdao score is greater than 0, calculate the pdao share @@ -862,15 +863,15 @@ func (r *treeGeneratorImpl_v11) calculateNodeRewards() (*nodeRewards, error) { // If voter score is greater than 0, calculate the voter share if r.totalVoterScore.Cmp(common.Big0) > 0 { - voterEth.Mul(r.smoothingPoolBalance, r.totalVoterScore) - voterEth.Div(voterEth, big.NewInt(int64(r.successfulAttestations))) - voterEth.Div(voterEth, oneEth) + voterEthFromSmoothingPool.Mul(r.smoothingPoolBalance, r.totalVoterScore) + voterEthFromSmoothingPool.Div(voterEthFromSmoothingPool, big.NewInt(int64(r.successfulAttestations))) + voterEthFromSmoothingPool.Div(voterEthFromSmoothingPool, oneEth) // Set the voter share eth in the rewards file - r.rewardsFile.TotalRewards.SmoothingPoolVoterShareEth.Set(voterEth) + r.rewardsFile.TotalRewards.SmoothingPoolVoterShareEth.Set(voterEthFromSmoothingPool) - // Add in the earmarked voter share - voterEth.Add(voterEth, r.networkState.NetworkDetails.SmoothingPoolPendingVoterShare) + // Add in the pending voter share (ETH distributed from Megapools) + totalVoterEth.Add(voterEthFromSmoothingPool, r.networkState.NetworkDetails.PendingVoterShareEth) } totalMegapoolVoteEligibleRpl := big.NewInt(0) @@ -893,16 +894,18 @@ func (r *treeGeneratorImpl_v11) calculateNodeRewards() (*nodeRewards, error) { // The node's voter share is nodeRpl*voterEth/totalMegapoolVoteEligibleRpl nodeInfo.VoterShareEth.Set(nodeInfo.MegapoolVoteEligibleRpl) - nodeInfo.VoterShareEth.Mul(nodeInfo.VoterShareEth, voterEth) + nodeInfo.VoterShareEth.Mul(nodeInfo.VoterShareEth, totalVoterEth) nodeInfo.VoterShareEth.Div(nodeInfo.VoterShareEth, totalMegapoolVoteEligibleRpl) trueVoterEth.Add(trueVoterEth, nodeInfo.VoterShareEth) } - // If there weren't any successful attestations, everything goes to the pool stakers + // If there weren't any successful attestations, everything goes to the pool stakers (rETH holders) if r.totalAttestationScore.Cmp(common.Big0) == 0 || r.successfulAttestations == 0 { r.log.Printlnf("WARNING: Total attestation score = %s, successful attestations = %d... sending the whole smoothing pool balance to the pool stakers.", r.totalAttestationScore.String(), r.successfulAttestations) poolStakerEth := big.NewInt(0).Set(r.smoothingPoolBalance) - poolStakerEth.Sub(poolStakerEth, trueVoterEth) + // We're distributing the smoothing pool balance plus the pending voter share + poolStakerEth.Add(poolStakerEth, r.networkState.NetworkDetails.PendingVoterShareEth) + poolStakerEth.Sub(poolStakerEth, totalVoterEth) poolStakerEth.Sub(poolStakerEth, pdaoEth) return &nodeRewards{ poolStakerEth: poolStakerEth, @@ -965,15 +968,13 @@ func (r *treeGeneratorImpl_v11) calculateNodeRewards() (*nodeRewards, error) { } if r.rewardsFile.RulesetVersion >= 10 { - remainingBalance := big.NewInt(0).Sub(r.smoothingPoolBalance, totalEthForMinipools) + // We're distributing the smoothing pool balance plus the pending voter share + remainingBalance := big.NewInt(0).Add(r.smoothingPoolBalance, r.networkState.NetworkDetails.PendingVoterShareEth) + remainingBalance.Sub(remainingBalance, totalEthForMinipools) remainingBalance.Sub(remainingBalance, totalEthForMegapools) remainingBalance.Sub(remainingBalance, pdaoEth) if trueVoterEth.Sign() > 0 { remainingBalance.Sub(remainingBalance, trueVoterEth) - } else { - // Nobody earned voter share. - // Subtract voter share- it shouldn't be used to pay bonuses, or we could have a deficit later. - remainingBalance.Sub(remainingBalance, r.networkState.NetworkDetails.SmoothingPoolPendingVoterShare) } // Only process bonuses if totalConsensusBonus is greater than zero if totalConsensusBonus != nil && totalConsensusBonus.Sign() > 0 { @@ -1024,8 +1025,9 @@ func (r *treeGeneratorImpl_v11) calculateNodeRewards() (*nodeRewards, error) { trueNodeOperatorAmount.Add(trueNodeOperatorAmount, totalEthForBonuses) - // This is how much actually goes to the pool stakers - it should ideally be equal to poolStakerShare but this accounts for any cumulative floating point errors - truePoolStakerAmount := big.NewInt(0).Sub(r.smoothingPoolBalance, trueNodeOperatorAmount) + // This is how much actually goes to the pool stakers (rETH holders) - it should ideally be equal to poolStakerShare but this accounts for any cumulative floating point errors + truePoolStakerAmount := big.NewInt(0).Add(r.smoothingPoolBalance, r.networkState.NetworkDetails.PendingVoterShareEth) + truePoolStakerAmount.Sub(truePoolStakerAmount, trueNodeOperatorAmount) truePoolStakerAmount.Sub(truePoolStakerAmount, pdaoEth) truePoolStakerAmount.Sub(truePoolStakerAmount, trueVoterEth) diff --git a/shared/services/rewards/mock_v11_test.go b/shared/services/rewards/mock_v11_test.go index 71dcb7d5f..224517135 100644 --- a/shared/services/rewards/mock_v11_test.go +++ b/shared/services/rewards/mock_v11_test.go @@ -850,7 +850,7 @@ func TestInsufficientEthForBonusesesV11(tt *testing.T) { // Ovewrite the SP balance to a value under the bonus commission history.NetworkDetails.SmoothingPoolBalance = big.NewInt(1100) // Set the SP voter share to 0 - history.NetworkDetails.SmoothingPoolPendingVoterShare = big.NewInt(100) + history.NetworkDetails.PendingVoterShareEth = big.NewInt(100) // Set the pdao share to 0 state := history.GetEndNetworkState() state.IsSaturnDeployed = true diff --git a/shared/services/rewards/test/mock.go b/shared/services/rewards/test/mock.go index 1b4e874ba..2bbc6059d 100644 --- a/shared/services/rewards/test/mock.go +++ b/shared/services/rewards/test/mock.go @@ -646,7 +646,7 @@ func NewDefaultMockHistoryNoNodes() *MockHistory { VoterShare: big.NewInt(6e16), PdaoShare: big.NewInt(5e16), }, - SmoothingPoolPendingVoterShare: big.NewInt(0).Mul(big.NewInt(10), oneEth), + PendingVoterShareEth: big.NewInt(0).Mul(big.NewInt(10), oneEth), // The rest of the fields seem unimportant and are left empty }, From 9f78c54f0f0ba7134f1304d0e6152a0b5ac64d33 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+0xfornax@users.noreply.github.com> Date: Tue, 20 Jan 2026 21:25:05 -0300 Subject: [PATCH 2/4] Don't send pending voter share to pool stakers --- bindings/utils/state/network.go | 2 +- shared/services/rewards/generator-impl-v11.go | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/bindings/utils/state/network.go b/bindings/utils/state/network.go index 720ae7da5..f1a304bff 100644 --- a/bindings/utils/state/network.go +++ b/bindings/utils/state/network.go @@ -82,7 +82,7 @@ type NetworkDetails struct { // Saturn MegapoolRevenueSplitSettings MegapoolRevenueSplitSettings MegapoolRevenueSplitTimeWeightedAverages MegapoolRevenueSplitTimeWeightedAverages - PendingVoterShareEth *big.Int `json:"pending_voter_share_eth"` + PendingVoterShareEth *big.Int `json:"pending_voter_share_eth"` } // Create a snapshot of all of the network's details diff --git a/shared/services/rewards/generator-impl-v11.go b/shared/services/rewards/generator-impl-v11.go index 4b16e3c83..f2ad46769 100644 --- a/shared/services/rewards/generator-impl-v11.go +++ b/shared/services/rewards/generator-impl-v11.go @@ -903,9 +903,7 @@ func (r *treeGeneratorImpl_v11) calculateNodeRewards() (*nodeRewards, error) { if r.totalAttestationScore.Cmp(common.Big0) == 0 || r.successfulAttestations == 0 { r.log.Printlnf("WARNING: Total attestation score = %s, successful attestations = %d... sending the whole smoothing pool balance to the pool stakers.", r.totalAttestationScore.String(), r.successfulAttestations) poolStakerEth := big.NewInt(0).Set(r.smoothingPoolBalance) - // We're distributing the smoothing pool balance plus the pending voter share - poolStakerEth.Add(poolStakerEth, r.networkState.NetworkDetails.PendingVoterShareEth) - poolStakerEth.Sub(poolStakerEth, totalVoterEth) + poolStakerEth.Sub(poolStakerEth, voterEthFromSmoothingPool) poolStakerEth.Sub(poolStakerEth, pdaoEth) return &nodeRewards{ poolStakerEth: poolStakerEth, From 20e2a2f213ffed8b296d23592b76b5cc46606428 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+fornax2@users.noreply.github.com> Date: Wed, 21 Jan 2026 00:39:24 -0300 Subject: [PATCH 3/4] Fix tests --- shared/services/rewards/mock_v11_test.go | 16 ++++++++-------- shared/types/eth2/fork/electra/state_electra.go | 2 +- shared/types/eth2/fork/fulu/state_fulu.go | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/shared/services/rewards/mock_v11_test.go b/shared/services/rewards/mock_v11_test.go index 224517135..2663dde6d 100644 --- a/shared/services/rewards/mock_v11_test.go +++ b/shared/services/rewards/mock_v11_test.go @@ -909,12 +909,12 @@ func TestInsufficientEthForBonusesesV11(tt *testing.T) { // Check the rewards file rewardsFile := v11Artifacts.RewardsFile ethOne := rewardsFile.GetNodeSmoothingPoolEth(nodeOne.Address) - if ethOne.Uint64() != 579 { - t.Fatalf("Node one ETH amount does not match expected value: %s != %d", ethOne.String(), 169+416) + if ethOne.Uint64() != 707 { + t.Fatalf("Node one ETH amount does not match expected value: %s != %d", ethOne.String(), 707) } ethTwo := rewardsFile.GetNodeSmoothingPoolEth(nodeTwo.Address) - if ethTwo.Uint64() != 420 { - t.Fatalf("Node two ETH amount does not match expected value: %s != %d", ethTwo.String(), 177+237) + if ethTwo.Uint64() != 492 { + t.Fatalf("Node two ETH amount does not match expected value: %s != %d", ethTwo.String(), 492) } // Check the minipool performance file @@ -923,15 +923,15 @@ func TestInsufficientEthForBonusesesV11(tt *testing.T) { if !ok { t.Fatalf("Node one minipool performance not found") } - if perfOne.GetBonusEthEarned().Uint64() != 393 { - t.Fatalf("Node one bonus does not match expected value: %s != %d", perfOne.GetBonusEthEarned().String(), 416) + if perfOne.GetBonusEthEarned().Uint64() != 521 { + t.Fatalf("Node one bonus does not match expected value: %s != %d", perfOne.GetBonusEthEarned().String(), 521) } perfTwo, ok := minipoolPerformanceFile.GetMinipoolPerformance(nodeTwo.Minipools[0].Address) if !ok { t.Fatalf("Node two minipool performance not found") } - if perfTwo.GetBonusEthEarned().Uint64() != 225 { - t.Fatalf("Node two bonus does not match expected value: %s != %d", perfTwo.GetBonusEthEarned().String(), 237) + if perfTwo.GetBonusEthEarned().Uint64() != 297 { + t.Fatalf("Node two bonus does not match expected value: %s != %d", perfTwo.GetBonusEthEarned().String(), 297) } } diff --git a/shared/types/eth2/fork/electra/state_electra.go b/shared/types/eth2/fork/electra/state_electra.go index 1248e9446..12eba6ab7 100644 --- a/shared/types/eth2/fork/electra/state_electra.go +++ b/shared/types/eth2/fork/electra/state_electra.go @@ -298,7 +298,7 @@ func (state *BeaconState) GetSlot() uint64 { return state.Slot } -// Added for compatibility +// Added for compatibility func (state *BeaconState) BlockHeaderProof() ([][]byte, error) { return nil, nil } diff --git a/shared/types/eth2/fork/fulu/state_fulu.go b/shared/types/eth2/fork/fulu/state_fulu.go index 9216c8afd..e22f66fff 100644 --- a/shared/types/eth2/fork/fulu/state_fulu.go +++ b/shared/types/eth2/fork/fulu/state_fulu.go @@ -283,7 +283,7 @@ func (state *BeaconState) BlockRootProof(slot uint64) ([][]byte, error) { if err != nil { return nil, fmt.Errorf("could not get proof for block root: %w", err) } - + return proof.Hashes, nil } From bc37e96412f265ac38e8183c43c887c794263fc5 Mon Sep 17 00:00:00 2001 From: Fornax <23104993+fornax2@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:09:21 -0300 Subject: [PATCH 4/4] Fix comment and voter share desc --- shared/services/rewards/generator-impl-v11.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/services/rewards/generator-impl-v11.go b/shared/services/rewards/generator-impl-v11.go index f2ad46769..94923712e 100644 --- a/shared/services/rewards/generator-impl-v11.go +++ b/shared/services/rewards/generator-impl-v11.go @@ -491,7 +491,7 @@ func (r *treeGeneratorImpl_v11) calculateEthRewards(checkBeaconPerformance bool) // Get the Smoothing Pool contract's balance r.smoothingPoolBalance = r.networkState.NetworkDetails.SmoothingPoolBalance r.log.Printlnf("%s Smoothing Pool Balance:\t%s\t(%.3f)", r.logPrefix, r.smoothingPoolBalance.String(), eth.WeiToEth(r.smoothingPoolBalance)) - r.log.Printlnf("%s Voter Share from Megapools:\t%s\t(%.3f)", r.logPrefix, r.networkState.NetworkDetails.PendingVoterShareEth.String(), eth.WeiToEth(r.networkState.NetworkDetails.PendingVoterShareEth)) + r.log.Printlnf("%s Voter Share from Megapools not in the smoothing pool:\t%s\t(%.3f)", r.logPrefix, r.networkState.NetworkDetails.PendingVoterShareEth.String(), eth.WeiToEth(r.networkState.NetworkDetails.PendingVoterShareEth)) if r.rewardsFile.Index == 0 { // This is the first interval, Smoothing Pool rewards are ignored on the first interval since it doesn't have a discrete start time @@ -892,7 +892,7 @@ func (r *treeGeneratorImpl_v11) calculateNodeRewards() (*nodeRewards, error) { continue } - // The node's voter share is nodeRpl*voterEth/totalMegapoolVoteEligibleRpl + // The node's voter share is nodeRpl*totalVoterEth/totalMegapoolVoteEligibleRpl nodeInfo.VoterShareEth.Set(nodeInfo.MegapoolVoteEligibleRpl) nodeInfo.VoterShareEth.Mul(nodeInfo.VoterShareEth, totalVoterEth) nodeInfo.VoterShareEth.Div(nodeInfo.VoterShareEth, totalMegapoolVoteEligibleRpl)