Skip to content

Add voter reward commission for validator groups#11694

Open
pahor167 wants to merge 12 commits intorelease/core-contracts/16from
pahor/voterValidatorRewardsCommision
Open

Add voter reward commission for validator groups#11694
pahor167 wants to merge 12 commits intorelease/core-contracts/16from
pahor/voterValidatorRewardsCommision

Conversation

@pahor167
Copy link
Copy Markdown
Collaborator

@pahor167 pahor167 commented Mar 11, 2026

Summary

Introduces a voter reward commission mechanism that allows validator groups to take a configurable percentage of voter CELO epoch rewards. This is separate from the existing commission field which governs the group's cut of validator cUSD payments.

Example: If a group sets 10% voter reward commission and voters for that group earn 1,000 CELO in epoch rewards, the group receives 100 CELO (released from CeloUnreleasedTreasury) and voters receive the remaining 900 CELO as vote credit inflation.

Changes

EpochManager.sol

  • _deductVoterRewardCommission() — deducts commission from voter rewards and releases CELO from treasury to the group
  • Commission is clamped to maxVoterRewardCommission at distribution time, so governance cap changes take effect immediately for all groups (even those with previously-activated higher rates)
  • Integrated into both processGroup() and finishNextEpochProcess() paths
  • New VoterRewardCommissionDistributed event

Validators.sol

  • New voterRewardCommission, nextVoterRewardCommission, nextVoterRewardCommissionBlock fields on ValidatorGroup struct
  • setNextVoterRewardCommissionUpdate() / updateVoterRewardCommission() — time-delayed queue/activate pattern matching existing commission flow, reusing commissionUpdateDelay
  • setMaxVoterRewardCommission() — governance-controlled cap (0 = no cap)
  • Max cap re-checked at activation time to handle governance lowering the cap after queuing
  • getVoterRewardCommission() — returns current, pending, and activation block

Economics

  • Commission is carved from the already-budgeted totalRewardsVoter pool — it redirects part of the voter reward from deferred LockedGold claims (vote credit inflation) to immediate treasury releases
  • The total economic cost per epoch is unchanged; commission changes the distribution mechanism, not the total emission

Tests

  • Unit tests covering: 0% commission, 10%, 100%, rounding-to-zero, sentinel values, both processing paths, event emission, queue/activate lifecycle, delay enforcement, max cap enforcement, cap-lowered-after-queue
  • Fuzz tests: conservation invariant (commission + voterRewards == totalRewards) across random commission rates and reward amounts, math verification against FixidityLib calculation
  • Epoch-timing tests: proving commission uses the rate active at processGroup() time, conservation holds when commission changes mid-processing
  • Max cap clamp tests: clamping at distribution time, no-clamp below cap, no-clamp when cap is 0, fuzz conservation with clamping

Storage Safety

  • ValidatorGroup struct fields appended at end (safe for mapping-based storage)
  • maxVoterRewardCommission state variable appended after deprecated_downtimeGracePeriod (safe for proxy upgrade)

Deployment Note

After upgrade, maxVoterRewardCommission defaults to 0 (no cap). The governance proposal should atomically call setMaxVoterRewardCommission() to set the desired cap.

Allows validator groups to take a percentage of voter CELO epoch rewards.
Adds voterRewardCommission fields to ValidatorGroup struct with time-delayed
update pattern, commission deduction in EpochManager before distributing
voter rewards, and comprehensive unit tests covering all edge cases.

Changes:
- Validators.sol: new struct fields, setNextVoterRewardCommissionUpdate(),
  updateVoterRewardCommission(), getVoterRewardCommission(),
  setMaxVoterRewardCommission(), version bump to (1,5,0,0)
- EpochManager.sol: _deductVoterRewardCommission() helper, commission
  deduction in processGroup() and finishNextEpochProcess(), version
  bump to (1,1,1,0)
- IValidators.sol: 5 new interface signatures
- MockValidators.sol: mock support for voter reward commission
- Validators.t.sol: 33 new tests across 4 test contracts
- EpochManager.t.sol: 7 new tests for commission distribution
@pahor167 pahor167 requested a review from a team as a code owner March 11, 2026 12:27
@pahor167
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Free Tier Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Bugbot Autofix is kicking off a free cloud agent to fix these issues. This run is complimentary, but you can enable autofix for all future PRs in the Cursor dashboard.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 608edba92c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Validators: (1,4,1,0) not (1,5,0,0) — additive changes = minor bump
EpochManager: (1,1,0,3) not (1,1,1,0) — internal logic change = patch bump
@pahor167
Copy link
Copy Markdown
Collaborator Author

review'd by Claude

🟡 Suggestion (3)

  • EpochManager version number not bumped despite new behaviorpackages/protocol/contracts-0.8/common/EpochManager.sol (line 626)
    The EpochManager version remains (1,1,0,3) — same as the base branch — despite adding new internal logic (_deductVoterRewardCommission), a new event (VoterRewardCommissionDistributed), and additional treasury releases. The second commit (1ad3668) reverted the version bump from (1,1,1,0) back to (1,1,0,3) with the rationale 'internal logic change = patch bump', but then didn't actually apply the patch bump. Even for internal changes, a patch bump (e.g., 1,1,0,4) is warranted since the contract's observable behavior changes (new event, different reward distribution, additional treasury releases).
  • updateVoterRewardCommission does not re-check maxVoterRewardCommissionpackages/protocol/contracts-0.8/governance/Validators.sol (line 525)
    When applying a queued voter reward commission update, the function does not re-validate against maxVoterRewardCommission. If governance lowers the cap between queue and activation (e.g., group queues 50%, governance sets max to 20%), the group can still activate the 50% commission. While this mirrors the existing updateCommission() pattern (which also doesn't re-check), the voter reward commission has a treasury cost impact that the regular commission doesn't — it directly releases CELO from treasury. Consider re-checking the cap at activation time, or at minimum document this as a known behavior.
  • Commission releases additional CELO from treasury beyond calculated epoch rewardspackages/protocol/contracts-0.8/common/EpochManager.sol (line 730)
    The voter reward commission is paid as additional CELO from CeloUnreleasedTreasury, beyond the amounts calculated by EpochRewards.calculateTargetEpochRewards(). Voter rewards are 'virtual' (they increase vote weight via Election.distributeEpochRewards without releasing CELO), but the commission converts a portion into real CELO. This means with e.g., 10% commission across all groups, the treasury releases an additional ~10% of totalRewardsVoter per epoch. While the code comments suggest this is intentional, the EpochRewards target calculations don't account for this additional inflation. If this is by design, consider adding a NatSpec comment on _deductVoterRewardCommission explaining this economic trade-off, since future maintainers will question it.

🔵 Nitpick (1)

  • Inconsistent variable naming between processGroup and finishNextEpochProcesspackages/protocol/contracts-0.8/common/EpochManager.sol (line 330)
    In processGroup(), the commission deduction uses explicit variable names (commissionAmount, voterRewards), while in finishNextEpochProcess() it reuses the epochRewards variable in-place (epochRewards -= _deductVoterRewardCommission(...)). Both approaches work correctly, but the explicit naming in processGroup is clearer. Consider using the same explicit pattern in finishNextEpochProcess for consistency and readability.

🟢 Good (2)

  • Clean implementation following established patternspackages/protocol/contracts-0.8/governance/Validators.sol (line 478)
    The queue/update pattern for voter reward commission closely mirrors the existing commission update flow, including the commission update delay, the _sendValidatorGroupPaymentsIfNecessary call before applying, and the max cap validation. This consistency makes the code predictable and easy to audit.
  • Thorough test coverage including edge casespackages/protocol/test-sol/unit/common/EpochManager.t.sol (line 990)
    The tests cover important edge cases: zero commission, full (100%) commission, commission that rounds to zero, the sentinel value (type(uint256).max for 0 rewards), and both processGroup and finishNextEpochProcess code paths. The Validators tests also cover revert cases, overwriting queued updates, and max commission enforcement.

Bottom line: 1. Bump EpochManager version — currently unchanged from base, should be at least (1,1,0,4). 2. Consider re-checking maxVoterRewardCommission at activation — the commission has direct treasury cost unlike the existing commission, so bypassing the cap is more consequential. 3. Document the economic model — the treasury releases additional CELO beyond the EpochRewards target calculations; this should be explicitly documented for future maintainers.

Automated review by reviewd. Findings are AI-generated — use your judgment.
Replies to this comment are not monitored.

- Remove unnecessary _sendValidatorGroupPaymentsIfNecessary call from
  updateVoterRewardCommission (voterRewardCommission is independent from
  cUSD validator payment commission)
- Re-check maxVoterRewardCommission at activation time to prevent cap
  bypass when governance lowers the cap between queue and activation
- Bump EpochManager version to (1,1,0,4) for new behavior
- Add NatSpec documenting treasury economic trade-off on
  _deductVoterRewardCommission
- Add tests for max cap re-check at activation
@pahor167
Copy link
Copy Markdown
Collaborator Author

Addressed in d2563ca:

  • Version bump: bumped to (1,1,0,4) ✅
  • Re-check maxVoterRewardCommission: added at activation time with tests ✅
  • Treasury docs: added NatSpec explaining the economic trade-off ✅
  • Naming inconsistency: can't use explicit vars due to stack-too-deep, added comment explaining why ✅

Pavel Hornak added 3 commits March 17, 2026 12:47
…ission

Add fuzz tests verifying conservation invariant (commission + voterRewards == totalRewards)
across random commission rates and reward amounts, plus tests proving that commission
changes during epoch processing use the rate active at processGroup() time rather than
epoch start. Also adds Validators fuzz tests for queue/activate lifecycle, max cap
enforcement, and cap-lowered-after-queue scenarios.
…tribution time

Previously, groups that activated a commission before governance lowered
maxVoterRewardCommission would keep collecting at the old (higher) rate.
Now _deductVoterRewardCommission clamps the effective rate to the cap,
ensuring governance changes take effect immediately for all groups.
…nomics

The previous comment incorrectly stated commission CELO was 'NOT accounted
for' in the reward budget. Commission is carved from the already-budgeted
totalRewardsVoter pool — it redirects part of the voter reward from deferred
LockedGold claims to immediate treasury releases, not additional emission.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 98720090ff

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Pavel Hornak and others added 5 commits March 17, 2026 14:52
Remove misleading dev note suggesting epoch-processing blocking is needed
(the 3-day commissionUpdateDelay makes this moot), and remove false claim
that voter reward commission is 'more consequential' than regular commission
(both ultimately cost the protocol).
…ardCommission

Governance should decide the initial cap, not the contract NatSpec.
Fuzz tests used 1e30 as upper bound for reward amounts, but the mock
treasury only holds ~3.07e26 CELO (L2_INITIAL_STASH_BALANCE). Large
fuzzed rewards caused commission releases to revert with 'Insufficient
balance'.
@pahor167 pahor167 changed the title feat(protocol): add voter reward commission for validator groups Add voter reward commission for validator groups Mar 17, 2026
…for validator allocation

startNextEpochProcess() calls allocateValidatorsRewards() which releases
CELO from treasury before processGroup(). Using full L2_INITIAL_STASH_BALANCE
as the reward bound means 100% commission can exceed what remains.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f75e95d1ee

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

uint256 epochRewards
) internal returns (uint256 commissionAmount) {
IValidators validators = getValidators();
(uint256 voterRewardCommissionUnwrapped, , ) = validators.getVoterRewardCommission(group);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard commission lookup against non-group addresses

processGroup/finishNextEpochProcess now always call validators.getVoterRewardCommission(group), but Validators.getVoterRewardCommission reverts unless isValidatorGroup(account) is true. A group can still appear in processedGroups from last-epoch membership and then be deregistered before reward processing (for configs where the empty-group duration allows it), which makes this lookup revert and blocks epoch finalization. Before this change, reward processing for such a group would not hard-fail on validator-group existence, so this introduces a liveness risk tied to deregistration timing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 72373833b8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

uint256 epochRewards
) internal returns (uint256 commissionAmount) {
IValidators validators = getValidators();
(uint256 voterRewardCommissionUnwrapped, , ) = validators.getVoterRewardCommission(group);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Tolerate non-registered groups during reward processing

_deductVoterRewardCommission unconditionally calls validators.getVoterRewardCommission(group), but that getter reverts unless the address is currently a validator group (Validators.sol enforces isValidatorGroup). Here group is taken from last-epoch membership, so if a group was elected last epoch and deregisters before rewards are processed (e.g., when empty-group duration is configured low enough), processGroup/finishNextEpochProcess will revert and epoch finalization can be blocked.

Useful? React with 👍 / 👎.

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.

1 participant