Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
16 changes: 15 additions & 1 deletion packages/protocol/contracts-0.8/common/EpochManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@ contract EpochManager is
struct EpochProcessState {
EpochProcessStatus status;
uint256 perValidatorReward; // The per validator epoch reward.
uint256 totalRewardsVoter; // The total rewards to voters.
uint256 totalRewardsVoter; // The total rewards to voters (target bucket).
uint256 totalRewardsCommunity; // The total community reward.
uint256 totalRewardsCarbonFund; // The total carbon offsetting partner reward.
// The sum of voter rewards actually distributed to elected groups during this
// epoch. May be less than `totalRewardsVoter` due to per-group score, slashing
// multipliers, and integer rounding in `Election.getGroupEpochRewardsBasedOnScore`.
uint256 totalDistributedVoterRewards;
Comment thread
pahor167 marked this conversation as resolved.
Outdated
}

bool public isSystemInitialized;
Expand Down Expand Up @@ -316,6 +320,7 @@ contract EpochManager is

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

delete processedGroups[group];
Expand Down Expand Up @@ -377,6 +382,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]);
_epochProcessing.totalDistributedVoterRewards += epochRewards;
}

delete processedGroups[groups[i]];
Expand Down Expand Up @@ -761,6 +767,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),
_epochProcessing.totalDistributedVoterRewards
);
celoUnreleasedTreasury.release(
registry.getAddressForOrDie(GOVERNANCE_REGISTRY_ID),
_epochProcessing.totalRewardsCommunity
Expand All @@ -775,6 +788,7 @@ contract EpochManager is
_epochProcessing.totalRewardsVoter = 0;
_epochProcessing.totalRewardsCommunity = 0;
_epochProcessing.totalRewardsCarbonFund = 0;
_epochProcessing.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