Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/protocol/contracts-0.8/common/EpochManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ contract EpochManager is

uint256 public toProcessGroups = 0;

// Sum of voter rewards actually distributed to elected groups during the
// current epoch. May be less than `epochProcessing.totalRewardsVoter` due to
// per-group score, slashing multipliers, and integer rounding in
// `Election.getGroupEpochRewardsBasedOnScore`.
// NOTE: declared after all pre-existing state variables to preserve the
// proxy storage layout on in-place upgrades.
Comment on lines +78 to +79
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This part of comment unnecessary.

uint256 public totalDistributedVoterRewards;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Detail but could be helpful: totalDistributedVoterRewards could be added as a view to IEpochManager.sol


/**
* @notice Event emitted when epochProcessing has begun.
* @param epochNumber The epoch number that is being processed.
Expand Down Expand Up @@ -316,6 +324,7 @@ contract EpochManager is

if (epochRewards != type(uint256).max) {
election.distributeEpochRewards(group, epochRewards, lesser, greater);
totalDistributedVoterRewards += epochRewards;
}

delete processedGroups[group];
Expand Down Expand Up @@ -377,6 +386,7 @@ contract EpochManager is
require(epochRewards > 0, "group not from current elected set");
if (epochRewards != type(uint256).max) {
election.distributeEpochRewards(groups[i], epochRewards, lessers[i], greaters[i]);
totalDistributedVoterRewards += epochRewards;
}

delete processedGroups[groups[i]];
Expand Down Expand Up @@ -761,6 +771,13 @@ contract EpochManager is
_setElectedSigners(_newlyElected);

ICeloUnreleasedTreasury celoUnreleasedTreasury = getCeloUnreleasedTreasury();
// Release only the voter rewards that were actually distributed to groups
// (post score, slashing multiplier, and rounding) to avoid stranding excess
// CELO in LockedGold without matching vote units.
celoUnreleasedTreasury.release(
registry.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID),
totalDistributedVoterRewards
);
celoUnreleasedTreasury.release(
registry.getAddressForOrDie(GOVERNANCE_REGISTRY_ID),
_epochProcessing.totalRewardsCommunity
Expand All @@ -775,6 +792,7 @@ contract EpochManager is
_epochProcessing.totalRewardsVoter = 0;
_epochProcessing.totalRewardsCommunity = 0;
_epochProcessing.totalRewardsCarbonFund = 0;
totalDistributedVoterRewards = 0;

emit EpochProcessingEnded(currentEpochNumber - 1);
}
Expand Down
76 changes: 76 additions & 0 deletions packages/protocol/test-sol/unit/common/EpochManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ contract EpochManagerTest is TestWithUtils08 {
address carbonOffsettingPartner;
address communityRewardFund;
address reserveAddress;
address lockedGoldAddress;
address scoreManagerAddress;

uint256 firstEpochNumber = 3;
Expand Down Expand Up @@ -90,6 +91,7 @@ contract EpochManagerTest is TestWithUtils08 {
scoreManagerAddress = actor("scoreManagerAddress");

reserveAddress = actor("reserve");
lockedGoldAddress = actor("lockedGold");

carbonOffsettingPartner = actor("carbonOffsettingPartner");
communityRewardFund = actor("communityRewardFund");
Expand All @@ -108,6 +110,7 @@ contract EpochManagerTest is TestWithUtils08 {
registry.setAddressFor(ScoreManagerContract, address(scoreManager));
registry.setAddressFor(StableTokenContract, address(stableToken));
registry.setAddressFor(ReserveContract, reserveAddress);
registry.setAddressFor(LockedGoldContract, lockedGoldAddress);
registry.setAddressFor(ElectionContract, address(election));

celoUnreleasedTreasury.setRegistry(REGISTRY_ADDRESS);
Expand Down Expand Up @@ -576,6 +579,56 @@ contract EpochManagerTest_finishNextEpochProcess is EpochManagerTest {

assertEq(celoToken.balanceOf(communityRewardFund), epochRewards.totalRewardsCommunity());
assertEq(celoToken.balanceOf(carbonOffsettingPartner), epochRewards.totalRewardsCarbonFund());
// LockedGold receives the sum of voter rewards actually distributed to groups,
// which for this fixture is `groupEpochRewards` (a single elected group).
assertEq(
celoToken.balanceOf(lockedGoldAddress),
groupEpochRewards,
"LockedGold should receive distributed voter rewards"
);
}

function test_ReleasesOnlyDistributedVoterRewards_WhenSlashed() public {
// Simulate a slashed/score-reduced group where Election distributes less than
// the target voter bucket. Release must match the distributed amount (not the
// target), otherwise excess CELO would be stranded in LockedGold.
uint256 reducedRewards = groupEpochRewards / 4;
election.setGroupEpochRewardsBasedOnScore(group, reducedRewards);

(
address[] memory groups,
address[] memory lessers,
address[] memory greaters
) = getGroupsWithLessersAndGreaters();

epochManagerContract.startNextEpochProcess();
epochManagerContract.finishNextEpochProcess(groups, lessers, greaters);

assertEq(
celoToken.balanceOf(lockedGoldAddress),
reducedRewards,
"LockedGold should receive only the distributed (reduced) voter rewards"
);
}

function test_ReleasesNothingToLockedGold_WhenAllGroupsIneligible() public {
// Group is ineligible / fully slashed -> distributed amount is 0.
election.setGroupEpochRewardsBasedOnScore(group, 0);

(
address[] memory groups,
address[] memory lessers,
address[] memory greaters
) = getGroupsWithLessersAndGreaters();

epochManagerContract.startNextEpochProcess();
epochManagerContract.finishNextEpochProcess(groups, lessers, greaters);

assertEq(
celoToken.balanceOf(lockedGoldAddress),
0,
"LockedGold should receive nothing when no voter rewards were distributed"
);
}

function test_TransfersToValidatorGroup() public {
Expand Down Expand Up @@ -741,6 +794,29 @@ contract EpochManagerTest_processGroup is EpochManagerTest {

assertEq(celoToken.balanceOf(communityRewardFund), epochRewards.totalRewardsCommunity());
assertEq(celoToken.balanceOf(carbonOffsettingPartner), epochRewards.totalRewardsCarbonFund());
assertEq(
celoToken.balanceOf(lockedGoldAddress),
groupEpochRewards,
"LockedGold should receive distributed voter rewards via processGroup path"
);
}

function test_ReleasesOnlyDistributedVoterRewards_WhenSlashed() public {
// Regression: per-group score / slashing multiplier reduces the distributed
// amount below the target voter bucket. Release must match what was actually
// distributed via the processGroup path.
Comment on lines +805 to +807
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure if comment is necessary. Do we need an in-code history of which unit tests were for regressions?

uint256 reducedRewards = groupEpochRewards / 4;
election.setGroupEpochRewardsBasedOnScore(group, reducedRewards);

epochManagerContract.startNextEpochProcess();
epochManagerContract.setToProcessGroups();
epochManagerContract.processGroup(group, address(0), address(0));

assertEq(
celoToken.balanceOf(lockedGoldAddress),
reducedRewards,
"LockedGold should receive only the distributed (reduced) voter rewards"
);
}

Comment thread
pahor167 marked this conversation as resolved.
function test_TransfersToValidatorGroup() public {
Expand Down
Loading