Skip to content

Commit 149352a

Browse files
author
ItsOtherMauridian
committed
fix: distribute reward payout remainder
Track settlement reward claims and assign any integer-division remainder to the final winning claimant so totalRewards is fully distributed. Add a regression test for the dust case described in #169.
1 parent 2de4d74 commit 149352a

2 files changed

Lines changed: 63 additions & 2 deletions

File tree

contracts/TruthBountyWeighted.sol

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ contract TruthBountyWeighted is AccessControl, ReentrancyGuard, Pausable, Govern
9696
uint256 totalSlashed;
9797
uint256 winnerWeightedStake; // Changed to weighted (NEW)
9898
uint256 loserWeightedStake; // Changed to weighted (NEW)
99+
uint256 winnerCount; // Number of winning voters eligible for rewards
100+
uint256 winnersClaimed; // Number of winning voters that claimed rewards
101+
uint256 rewardsClaimed; // Total rewards already distributed
99102
}
100103

101104
struct VerifierStake {
@@ -340,9 +343,17 @@ contract TruthBountyWeighted is AccessControl, ReentrancyGuard, Pausable, Govern
340343
bool isWinner = (vote.support == settlement.passed);
341344
require(isWinner, "Not a winner");
342345

343-
// Calculate proportional reward based on EFFECTIVE stake
346+
// Calculate proportional reward based on EFFECTIVE stake. Integer division can
347+
// leave a remainder, so assign any undistributed dust to the final winning
348+
// claimant to ensure totalRewards is fully paid out.
344349
uint256 reward = (vote.effectiveStake * settlement.totalRewards) / settlement.winnerWeightedStake;
345350

351+
settlement.winnersClaimed += 1;
352+
if (settlement.winnersClaimed == settlement.winnerCount) {
353+
reward = settlement.totalRewards - settlement.rewardsClaimed;
354+
}
355+
settlement.rewardsClaimed += reward;
356+
346357
// Mark as claimed
347358
vote.rewardClaimed = true;
348359

@@ -518,10 +529,27 @@ contract TruthBountyWeighted is AccessControl, ReentrancyGuard, Pausable, Govern
518529
totalRewards: rewardAmount,
519530
totalSlashed: slashedAmount,
520531
winnerWeightedStake: winnerWeightedStake,
521-
loserWeightedStake: loserWeightedStake
532+
loserWeightedStake: loserWeightedStake,
533+
winnerCount: _countWinners(claimId, passed),
534+
winnersClaimed: 0,
535+
rewardsClaimed: 0
522536
});
523537
}
524538

539+
/**
540+
* @notice Count voters on the winning side for remainder-safe reward distribution
541+
*/
542+
function _countWinners(uint256 claimId, bool passed) internal view returns (uint256 count) {
543+
address[] storage voters = claimVoters[claimId];
544+
545+
for (uint256 i = 0; i < voters.length; i++) {
546+
Vote storage vote = votes[claimId][voters[i]];
547+
if (vote.support == passed) {
548+
count += 1;
549+
}
550+
}
551+
}
552+
525553
/**
526554
* @notice Assign per-vote slash amounts to each loser
527555
* @dev Iterates through all voters and stores slash amount in Vote struct for losers

test/TruthBountyWeighted.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,39 @@ describe("TruthBountyWeighted", function () {
309309
expect(reward1).to.be.closeTo(verifier1RewardShare, ethers.parseEther("0.01"));
310310
expect(reward2).to.be.closeTo(verifier2RewardShare, ethers.parseEther("0.01"));
311311
});
312+
313+
it("Should fully distribute reward remainder instead of leaving dust", async function () {
314+
await truthBounty.setMinStakeAmount(1);
315+
await truthBounty.setSlashPercent(100);
316+
await truthBounty.setRewardPercent(100);
317+
318+
await mockOracle.setReputationScore(await verifier1.getAddress(), ethers.parseEther("1"));
319+
await mockOracle.setReputationScore(await verifier2.getAddress(), ethers.parseEther("2"));
320+
await mockOracle.setReputationScore(await verifier3.getAddress(), ethers.parseEther("0.1"));
321+
322+
await truthBounty.connect(verifier1).vote(claimId, true, 1);
323+
await truthBounty.connect(verifier2).vote(claimId, true, 1);
324+
await truthBounty.connect(verifier3).vote(claimId, false, 10);
325+
326+
await time.increase(VERIFICATION_WINDOW + 1);
327+
await truthBounty.settleClaim(claimId);
328+
329+
const settlement = await truthBounty.settlementResults(claimId);
330+
expect(settlement.totalRewards).to.equal(10);
331+
332+
const balanceBefore1 = await bountyToken.balanceOf(await verifier1.getAddress());
333+
await truthBounty.connect(verifier1).claimSettlementRewards(claimId);
334+
const balanceAfter1 = await bountyToken.balanceOf(await verifier1.getAddress());
335+
336+
const balanceBefore2 = await bountyToken.balanceOf(await verifier2.getAddress());
337+
await truthBounty.connect(verifier2).claimSettlementRewards(claimId);
338+
const balanceAfter2 = await bountyToken.balanceOf(await verifier2.getAddress());
339+
340+
const reward1 = balanceAfter1 - balanceBefore1 - 1n;
341+
const reward2 = balanceAfter2 - balanceBefore2 - 1n;
342+
343+
expect(reward1 + reward2).to.equal(settlement.totalRewards);
344+
});
312345
});
313346

314347
describe("Pause Guards", function () {

0 commit comments

Comments
 (0)