-
Notifications
You must be signed in to change notification settings - Fork 476
Add voter reward commission for validator groups #11694
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release/core-contracts/17
Are you sure you want to change the base?
Changes from 6 commits
608edba
1ad3668
d2563ca
f9ec8e8
0e23fde
9872009
08a0cdb
47b0bb3
f7e95c5
b5a1379
f75e95d
7237383
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -139,6 +139,18 @@ contract EpochManager is | |
| uint256 indexed epochNumber | ||
| ); | ||
|
|
||
| /** | ||
| * @notice Emitted when voter reward commission is distributed to a group. | ||
| * @param group Address of the validator group receiving commission. | ||
| * @param commission Amount of CELO released to the group as commission. | ||
| * @param epochNumber The epoch number for which the commission is distributed. | ||
| */ | ||
| event VoterRewardCommissionDistributed( | ||
| address indexed group, | ||
| uint256 commission, | ||
| uint256 indexed epochNumber | ||
| ); | ||
|
|
||
| /** | ||
| * @notice Throws if called by other than EpochManagerEnabler contract. | ||
| */ | ||
|
|
@@ -315,7 +327,11 @@ contract EpochManager is | |
| IElection election = getElection(); | ||
|
|
||
| if (epochRewards != type(uint256).max) { | ||
| election.distributeEpochRewards(group, epochRewards, lesser, greater); | ||
| uint256 commissionAmount = _deductVoterRewardCommission(group, epochRewards); | ||
| uint256 voterRewards = epochRewards - commissionAmount; | ||
| if (voterRewards > 0) { | ||
| election.distributeEpochRewards(group, voterRewards, lesser, greater); | ||
| } | ||
| } | ||
|
|
||
| delete processedGroups[group]; | ||
|
|
@@ -376,7 +392,12 @@ contract EpochManager is | |
| // checks that group is actually from elected group | ||
| require(epochRewards > 0, "group not from current elected set"); | ||
| if (epochRewards != type(uint256).max) { | ||
| election.distributeEpochRewards(groups[i], epochRewards, lessers[i], greaters[i]); | ||
| // Note: Uses in-place subtraction instead of explicit variable names (as in processGroup) | ||
| // to avoid stack-too-deep in this function which has more local variables. | ||
| epochRewards -= _deductVoterRewardCommission(groups[i], epochRewards); | ||
| if (epochRewards > 0) { | ||
| election.distributeEpochRewards(groups[i], epochRewards, lessers[i], greaters[i]); | ||
| } | ||
| } | ||
|
|
||
| delete processedGroups[groups[i]]; | ||
|
|
@@ -604,7 +625,7 @@ contract EpochManager is | |
| * @return Patch version of the contract. | ||
| */ | ||
| function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { | ||
| return (1, 1, 0, 3); | ||
| return (1, 1, 0, 4); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -682,6 +703,52 @@ contract EpochManager is | |
| return (_epoch.firstBlock, _epoch.lastBlock, _epoch.startTimestamp, _epoch.rewardsBlock); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Deducts voter reward commission for a group and releases CELO from treasury to group. | ||
| * @param group The validator group address. | ||
| * @param epochRewards The total voter epoch rewards for this group. | ||
| * @return commissionAmount The amount deducted as commission. | ||
| * @dev ECONOMIC NOTE: Voter rewards are normally distributed as vote credit inflation via | ||
| * Election.distributeEpochRewards(), which creates deferred claims on the LockedGold pool | ||
| * redeemable when voters revoke and withdraw. This commission converts a portion of the | ||
| * already-budgeted totalRewardsVoter into an immediate CELO release from CeloUnreleasedTreasury. | ||
| * The total economic cost is unchanged — commission redirects part of the voter reward budget | ||
| * from deferred LockedGold claims to immediate treasury releases. The per-epoch treasury outflow | ||
| * from commission equals the sum of (groupVoterRewards * groupCommission) across all elected | ||
| * groups, bounded by maxVoterRewardCommission. | ||
| */ | ||
| function _deductVoterRewardCommission( | ||
| address group, | ||
| uint256 epochRewards | ||
| ) internal returns (uint256 commissionAmount) { | ||
| IValidators validators = getValidators(); | ||
| (uint256 voterRewardCommissionUnwrapped, , ) = validators.getVoterRewardCommission(group); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
|
|
||
| if (voterRewardCommissionUnwrapped == 0) { | ||
| return 0; | ||
| } | ||
|
|
||
| // Clamp to the governance-set max cap so that previously-activated commissions | ||
| // exceeding a later-lowered cap are still bounded at distribution time. | ||
| uint256 maxCommission = validators.maxVoterRewardCommission(); | ||
|
pahor167 marked this conversation as resolved.
|
||
| if (maxCommission > 0 && voterRewardCommissionUnwrapped > maxCommission) { | ||
| voterRewardCommissionUnwrapped = maxCommission; | ||
| } | ||
|
|
||
| commissionAmount = FixidityLib | ||
| .newFixed(epochRewards) | ||
| .multiply(FixidityLib.wrap(voterRewardCommissionUnwrapped)) | ||
| .fromFixed(); | ||
|
|
||
| if (commissionAmount > 0) { | ||
| // Release CELO from treasury directly to the group. | ||
| // This mirrors the pattern used for community and carbon fund rewards | ||
| // in _finishEpochHelper(). | ||
| getCeloUnreleasedTreasury().release(group, commissionAmount); | ||
| emit VoterRewardCommissionDistributed(group, commissionAmount, currentEpochNumber); | ||
|
pahor167 marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @notice Allocates rewards to elected validator accounts. | ||
| */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,6 +66,11 @@ contract Validators is | |
| // sizeHistory[i] contains the last time the group contained i members. | ||
| uint256[] sizeHistory; | ||
| SlashingInfo slashInfo; | ||
| // Commission on voter CELO rewards (independent from validator payment commission above). | ||
| // Groups set this to take a percentage of epoch rewards that would otherwise go to voters. | ||
| FixidityLib.Fraction voterRewardCommission; | ||
| FixidityLib.Fraction nextVoterRewardCommission; | ||
| uint256 nextVoterRewardCommissionBlock; | ||
| } | ||
|
|
||
| // Stores the epoch number at which a validator joined a particular group. | ||
|
|
@@ -129,6 +134,11 @@ contract Validators is | |
| uint256 public slashingMultiplierResetPeriod; | ||
| uint256 public deprecated_downtimeGracePeriod; | ||
|
|
||
| // Cap on voter reward commission to protect voters from excessive commission rates. | ||
| // Set via governance. A value of 0 means no cap is enforced. | ||
| // Recommended initial value: 20% (FixidityLib representation). | ||
| uint256 public maxVoterRewardCommission; | ||
|
|
||
| event MaxGroupSizeSet(uint256 size); | ||
| event CommissionUpdateDelaySet(uint256 delay); | ||
| event GroupLockedGoldRequirementsSet(uint256 value, uint256 duration); | ||
|
|
@@ -150,6 +160,13 @@ contract Validators is | |
| uint256 activationBlock | ||
| ); | ||
| event ValidatorGroupCommissionUpdated(address indexed group, uint256 commission); | ||
| event ValidatorGroupVoterRewardCommissionUpdateQueued( | ||
| address indexed group, | ||
| uint256 commission, | ||
| uint256 activationBlock | ||
| ); | ||
| event ValidatorGroupVoterRewardCommissionUpdated(address indexed group, uint256 commission); | ||
| event MaxVoterRewardCommissionSet(uint256 maxCommission); | ||
|
|
||
| modifier onlySlasher() { | ||
| require(getLockedGold().isSlasher(msg.sender), "Only registered slasher can call"); | ||
|
|
@@ -452,6 +469,73 @@ contract Validators is | |
| emit ValidatorGroupCommissionUpdated(account, group.commission.unwrap()); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Queues an update to a validator group's voter reward commission. | ||
| * If there was a previously scheduled update, that is overwritten. | ||
| * @param commission Fixidity representation of the commission this group receives on epoch | ||
| * voter rewards. Must be in the range [0, 1.0] and below maxVoterRewardCommission if set. | ||
| */ | ||
| function setNextVoterRewardCommissionUpdate(uint256 commission) external { | ||
| address account = getAccounts().validatorSignerToAccount(msg.sender); | ||
| require(isValidatorGroup(account), "Not a validator group"); | ||
| ValidatorGroup storage group = groups[account]; | ||
| require( | ||
| commission <= FixidityLib.fixed1().unwrap(), | ||
| "Voter reward commission can't be greater than 100%" | ||
| ); | ||
| if (maxVoterRewardCommission > 0) { | ||
| require( | ||
| commission <= maxVoterRewardCommission, | ||
| "Voter reward commission exceeds max allowed" | ||
| ); | ||
| } | ||
| require( | ||
| commission != group.voterRewardCommission.unwrap(), | ||
| "Voter reward commission must be different" | ||
| ); | ||
|
|
||
| group.nextVoterRewardCommission = FixidityLib.wrap(commission); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: wrap into Fixidity earlier in function, then the comparisons in checks above don't need |
||
| group.nextVoterRewardCommissionBlock = block.number.add(commissionUpdateDelay); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we still using SafeMath to keep with old conventions, or should we use 0.8's default checked arithmetic in new logic? |
||
| emit ValidatorGroupVoterRewardCommissionUpdateQueued( | ||
| account, | ||
| commission, | ||
| group.nextVoterRewardCommissionBlock | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Updates a validator group's voter reward commission based on the previously queued | ||
| * update. | ||
| * @dev Note: Consider adding onlyWhenNotBlocked modifier if Validators inherits Blockable | ||
| * in the future, to prevent updates during epoch processing. | ||
| */ | ||
| function updateVoterRewardCommission() external { | ||
|
pahor167 marked this conversation as resolved.
|
||
| address account = getAccounts().validatorSignerToAccount(msg.sender); | ||
| require(isValidatorGroup(account), "Not a validator group"); | ||
| ValidatorGroup storage group = groups[account]; | ||
|
|
||
| require(group.nextVoterRewardCommissionBlock != 0, "No voter reward commission update queued"); | ||
| require( | ||
| group.nextVoterRewardCommissionBlock <= block.number, | ||
| "Can't apply voter reward commission update yet" | ||
| ); | ||
|
|
||
| // Re-check max cap at activation time. Governance may have lowered the cap since the | ||
| // update was queued, and unlike regular commission, voter reward commission directly | ||
| // releases CELO from treasury — so bypassing the cap is more consequential. | ||
| if (maxVoterRewardCommission > 0) { | ||
| require( | ||
| group.nextVoterRewardCommission.unwrap() <= maxVoterRewardCommission, | ||
| "Voter reward commission exceeds max allowed" | ||
| ); | ||
| } | ||
|
|
||
| group.voterRewardCommission = group.nextVoterRewardCommission; | ||
|
pahor167 marked this conversation as resolved.
|
||
| delete group.nextVoterRewardCommission; | ||
| delete group.nextVoterRewardCommissionBlock; | ||
| emit ValidatorGroupVoterRewardCommissionUpdated(account, group.voterRewardCommission.unwrap()); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Removes a validator from the group for which it is a member. | ||
| * @param validatorAccount The validator to deaffiliate from their affiliated validator group. | ||
|
|
@@ -541,6 +625,25 @@ contract Validators is | |
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Returns the voter reward commission for a validator group. | ||
| * @param account The address of the validator group. | ||
| * @return The current voter reward commission (Fixidity). | ||
| * @return The queued voter reward commission (Fixidity). | ||
| * @return The block at which the queued commission activates. | ||
| */ | ||
| function getVoterRewardCommission( | ||
| address account | ||
| ) external view returns (uint256, uint256, uint256) { | ||
| require(isValidatorGroup(account), "Not a validator group"); | ||
| ValidatorGroup storage group = groups[account]; | ||
| return ( | ||
| group.voterRewardCommission.unwrap(), | ||
| group.nextVoterRewardCommission.unwrap(), | ||
| group.nextVoterRewardCommissionBlock | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Returns the top n group members for a particular group. | ||
| * @param account The address of the validator group. | ||
|
|
@@ -779,7 +882,7 @@ contract Validators is | |
| * @return Patch version of the contract. | ||
| */ | ||
| function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { | ||
| return (1, 4, 0, 1); | ||
| return (1, 4, 1, 0); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -821,6 +924,22 @@ contract Validators is | |
| emit CommissionUpdateDelaySet(delay); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Sets the maximum voter reward commission that groups can set. | ||
| * @param maxCommission Fixidity representation of the max commission. | ||
| * A value of 0 means no cap is enforced. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wondering if this is the right approach. Having no special value would be conceptually simpler and
|
||
| * Recommended initial value: 20% (FixidityLib representation). | ||
| */ | ||
| function setMaxVoterRewardCommission(uint256 maxCommission) external onlyOwner { | ||
| require( | ||
| maxCommission <= FixidityLib.fixed1().unwrap(), | ||
| "Max voter reward commission can't be greater than 100%" | ||
| ); | ||
| require(maxCommission != maxVoterRewardCommission, "Max voter reward commission not changed"); | ||
| maxVoterRewardCommission = maxCommission; | ||
| emit MaxVoterRewardCommissionSet(maxCommission); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Updates the maximum number of members a group can have. | ||
| * @param size The maximum group size. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
processGroup/finishNextEpochProcessnow always callvalidators.getVoterRewardCommission(group), butValidators.getVoterRewardCommissionreverts unlessisValidatorGroup(account)is true. A group can still appear inprocessedGroupsfrom 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 👍 / 👎.