Skip to content

Commit adbb704

Browse files
author
Alex Sedighi
authored
feat(Rewards): give incentive to batch submitter (#41)
1 parent 80c6276 commit adbb704

File tree

2 files changed

+160
-9
lines changed

2 files changed

+160
-9
lines changed

src/Rewards.sol

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ contract Rewards is AccessControl, EIP712 {
9494
* @dev The sequence number of the batch reward.
9595
*/
9696
uint256 public batchSequence;
97+
/**
98+
* @dev Determines the amount of rewards to be minted for the submitter of batch as a percentage of total rewards in that batch.
99+
* This value indicates the cost overhead of minting rewards that the network is happy to take. Though it is set to 2% by default,
100+
* the governance can change it to ensure the network is sustainable.
101+
*/
102+
uint256 public batchSubmitterRewardPercentage;
97103

98104
/**
99105
* @dev Error when the reward quota is exceeded.
@@ -124,6 +130,10 @@ contract Rewards is AccessControl, EIP712 {
124130
* The recipient and amounts arrays must have the same length.
125131
*/
126132
error InvalidBatchStructure();
133+
/**
134+
* @dev Error thrown when the value is out of range. For example for Percentage values should be less than 100.
135+
*/
136+
error OutOfRangeValue();
127137

128138
/**
129139
* @dev Event emitted when the reward quota is set.
@@ -147,9 +157,13 @@ contract Rewards is AccessControl, EIP712 {
147157
* @param initialPeriod Initial reward period.
148158
* @param oracleAddress Address of the authorized oracle.
149159
*/
150-
constructor(NODL token, uint256 initialQuota, uint256 initialPeriod, address oracleAddress)
151-
EIP712(SIGNING_DOMAIN, SIGNATURE_VERSION)
152-
{
160+
constructor(
161+
NODL token,
162+
uint256 initialQuota,
163+
uint256 initialPeriod,
164+
address oracleAddress,
165+
uint256 rewardPercentage
166+
) EIP712(SIGNING_DOMAIN, SIGNATURE_VERSION) {
153167
// This is to avoid the ongoinb overhead of safe math operations
154168
if (initialPeriod == 0) {
155169
revert ZeroPeriod();
@@ -159,13 +173,16 @@ contract Rewards is AccessControl, EIP712 {
159173
revert TooLongPeriod();
160174
}
161175

176+
_mustBeLessThan100(batchSubmitterRewardPercentage);
177+
162178
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
163179

164180
nodl = token;
165181
quota = initialQuota;
166182
period = initialPeriod;
167183
quotaRenewalTimestamp = block.timestamp + period;
168184
authorizedOracle = oracleAddress;
185+
batchSubmitterRewardPercentage = rewardPercentage;
169186
}
170187

171188
/**
@@ -208,19 +225,36 @@ contract Rewards is AccessControl, EIP712 {
208225
_mustBeFromAuthorizedOracle(digestBatchReward(batch), signature);
209226

210227
_checkedUpdateQuota();
228+
211229
uint256 batchSum = _batchSum(batch);
212-
_checkedUpdateClaimed(batchSum);
230+
uint256 submitterRewardAmount = (batchSum * batchSubmitterRewardPercentage) / 100;
231+
232+
_checkedUpdateClaimed(batchSum + submitterRewardAmount);
213233

214234
// Safe to increment the sequence after checking this is the expected number (no overflow for the age of universe even with 1000 reward claims per second)
215235
batchSequence = batch.sequence + 1;
216236

217237
for (uint256 i = 0; i < batch.recipients.length; i++) {
218238
nodl.mint(batch.recipients[i], batch.amounts[i]);
219239
}
240+
nodl.mint(msg.sender, submitterRewardAmount);
220241

221242
emit BatchMinted(batchSum, claimed);
222243
}
223244

245+
/**
246+
* @dev Sets the reward percentage for the batch submitter.
247+
* @param newPercentage The new reward percentage to be set.
248+
* Requirements:
249+
* - Caller must have the DEFAULT_ADMIN_ROLE.
250+
* - The new reward percentage must be less than 100.
251+
*/
252+
function setBatchSubmitterRewardPercentage(uint256 newPercentage) external {
253+
_checkRole(DEFAULT_ADMIN_ROLE);
254+
_mustBeLessThan100(newPercentage);
255+
batchSubmitterRewardPercentage = newPercentage;
256+
}
257+
224258
/**
225259
* @dev Internal function to update the rewards quota if the current block timestamp is greater than or equal to the quota renewal timestamp.
226260
* @notice This function resets the rewards claimed to 0 and updates the quota renewal timestamp based on the reward period.
@@ -251,6 +285,17 @@ contract Rewards is AccessControl, EIP712 {
251285
claimed = newClaimed;
252286
}
253287

288+
/**
289+
* @dev Checks if the given percentage value is less than or equal to 100.
290+
* @param percent The percentage value to check.
291+
* @dev Throws an exception if the value is greater than 100.
292+
*/
293+
function _mustBeLessThan100(uint256 percent) internal pure {
294+
if (percent > 100) {
295+
revert OutOfRangeValue();
296+
}
297+
}
298+
254299
/**
255300
* @dev Internal check to ensure the `sequence` value is expected for `receipent`.
256301
* @param receipent The address of the receipent to check.

test/Rewards.t.sol

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ contract RewardsTest is Test {
2424
address oracle = vm.addr(oraclePrivateKey);
2525

2626
nodlToken = new NODL();
27-
rewards = new Rewards(nodlToken, 1000, RENEWAL_PERIOD, oracle);
27+
rewards = new Rewards(nodlToken, 1000, RENEWAL_PERIOD, oracle, 2);
2828
// Grant MINTER_ROLE to the Rewards contract
2929
nodlToken.grantRole(nodlToken.MINTER_ROLE(), address(rewards));
3030
}
@@ -98,23 +98,27 @@ contract RewardsTest is Test {
9898
}
9999

100100
function test_mintBatchReward() public {
101+
address submitter = address(44);
101102
address[] memory recipients = new address[](2);
102103
uint256[] memory amounts = new uint256[](2);
103104

104105
recipients[0] = address(11);
105106
recipients[1] = address(22);
106107
amounts[0] = 100;
107108
amounts[1] = 200;
109+
uint256 submitterReward = (amounts[0] + amounts[1]) * rewards.batchSubmitterRewardPercentage() / 100;
108110

109111
Rewards.BatchReward memory rewardsBatch = Rewards.BatchReward(recipients, amounts, 0);
110112

111113
bytes memory signature = createBatchSignature(rewardsBatch, oraclePrivateKey);
112114

115+
vm.prank(submitter);
113116
rewards.mintBatchReward(rewardsBatch, signature);
114117

115-
assertEq(nodlToken.balanceOf(recipients[0]), 100);
116-
assertEq(nodlToken.balanceOf(recipients[1]), 200);
117-
assertEq(rewards.claimed(), 300);
118+
assertEq(nodlToken.balanceOf(recipients[0]), amounts[0]);
119+
assertEq(nodlToken.balanceOf(recipients[1]), amounts[1]);
120+
assertEq(nodlToken.balanceOf(submitter), submitterReward);
121+
assertEq(rewards.claimed(), amounts[0] + amounts[1] + submitterReward);
118122
assertEq(rewards.sequences(recipients[0]), 0);
119123
assertEq(rewards.sequences(recipients[1]), 0);
120124
assertEq(rewards.batchSequence(), 1);
@@ -315,7 +319,7 @@ contract RewardsTest is Test {
315319
Rewards.BatchReward memory rewardsBatch = Rewards.BatchReward(recipients, amounts, 0);
316320
bytes memory batchSignature = createBatchSignature(rewardsBatch, oraclePrivateKey);
317321
rewards.mintBatchReward(rewardsBatch, batchSignature);
318-
assertEq(rewards.claimed(), 300);
322+
assertEq(rewards.claimed(), 300 + 300 * rewards.batchSubmitterRewardPercentage() / 100);
319323

320324
assertEq(rewards.quotaRenewalTimestamp(), sixthRenewal);
321325
}
@@ -354,6 +358,108 @@ contract RewardsTest is Test {
354358
assertEq(rewards.claimed(), 30);
355359
}
356360

361+
function test_setBatchSubmitterRewardPercentage() public {
362+
address alice = address(2);
363+
rewards.grantRole(rewards.DEFAULT_ADMIN_ROLE(), alice);
364+
365+
assertEq(rewards.batchSubmitterRewardPercentage(), 2);
366+
367+
vm.prank(alice);
368+
rewards.setBatchSubmitterRewardPercentage(10);
369+
370+
assertEq(rewards.batchSubmitterRewardPercentage(), 10);
371+
}
372+
373+
function test_setBatchSubmitterRewardPercentageUnauthorized() public {
374+
address bob = address(3);
375+
vm.expectRevert_AccessControlUnauthorizedAccount(bob, rewards.DEFAULT_ADMIN_ROLE());
376+
vm.prank(bob);
377+
rewards.setBatchSubmitterRewardPercentage(10);
378+
}
379+
380+
function test_setBatchSubmitterRewardPercentageOutOfRange() public {
381+
address alice = address(2);
382+
rewards.grantRole(rewards.DEFAULT_ADMIN_ROLE(), alice);
383+
384+
vm.expectRevert(Rewards.OutOfRangeValue.selector);
385+
vm.prank(alice);
386+
rewards.setBatchSubmitterRewardPercentage(101);
387+
}
388+
389+
function test_changingSubmitterRewardPercentageIsEffective() public {
390+
address alice = address(2);
391+
rewards.grantRole(rewards.DEFAULT_ADMIN_ROLE(), alice);
392+
393+
assertEq(rewards.batchSubmitterRewardPercentage(), 2);
394+
395+
vm.prank(alice);
396+
rewards.setBatchSubmitterRewardPercentage(100);
397+
398+
assertEq(rewards.batchSubmitterRewardPercentage(), 100);
399+
400+
address[] memory recipients = new address[](2);
401+
uint256[] memory amounts = new uint256[](2);
402+
recipients[0] = address(11);
403+
recipients[1] = address(22);
404+
amounts[0] = 100;
405+
amounts[1] = 200;
406+
Rewards.BatchReward memory rewardsBatch = Rewards.BatchReward(recipients, amounts, 0);
407+
bytes memory batchSignature = createBatchSignature(rewardsBatch, oraclePrivateKey);
408+
vm.prank(alice);
409+
rewards.mintBatchReward(rewardsBatch, batchSignature);
410+
411+
assertEq(rewards.claimed(), 2 * 300); // expected value for 100% submitter reward
412+
assertEq(nodlToken.balanceOf(alice), 300);
413+
}
414+
415+
function test_mintBatchRewardOverflowsOnBatchSum() public {
416+
address[] memory recipients = new address[](2);
417+
uint256[] memory amounts = new uint256[](2);
418+
419+
recipients[0] = address(11);
420+
recipients[1] = address(22);
421+
amounts[0] = type(uint256).max;
422+
amounts[1] = 1;
423+
424+
Rewards.BatchReward memory rewardsBatch = Rewards.BatchReward(recipients, amounts, 0);
425+
426+
bytes memory signature = createBatchSignature(rewardsBatch, oraclePrivateKey);
427+
vm.expectRevert(stdError.arithmeticError);
428+
rewards.mintBatchReward(rewardsBatch, signature);
429+
}
430+
431+
function test_mintBatchRewardOverflowsOnSubmitterReward() public {
432+
address alice = address(2);
433+
rewards.grantRole(rewards.DEFAULT_ADMIN_ROLE(), alice);
434+
435+
// Ensure the quota is high enough
436+
vm.prank(alice);
437+
rewards.setQuota(type(uint256).max);
438+
439+
// Check initial submitter reward percentage
440+
assertEq(rewards.batchSubmitterRewardPercentage(), 2);
441+
442+
address[] memory recipients = new address[](1);
443+
uint256[] memory amounts = new uint256[](1);
444+
445+
recipients[0] = address(11);
446+
// This amount, even though high, should not cause an overflow
447+
amounts[0] = type(uint256).max / (2 * rewards.batchSubmitterRewardPercentage());
448+
449+
Rewards.BatchReward memory rewardsBatch = Rewards.BatchReward(recipients, amounts, 0);
450+
bytes memory signature = createBatchSignature(rewardsBatch, oraclePrivateKey);
451+
rewards.mintBatchReward(rewardsBatch, signature);
452+
453+
vm.prank(alice);
454+
// Same batch sum should now cause an overflow in the submitter reward calculation
455+
rewards.setBatchSubmitterRewardPercentage(100);
456+
457+
Rewards.BatchReward memory rewardsBatch2 = Rewards.BatchReward(recipients, amounts, 1);
458+
bytes memory signature2 = createBatchSignature(rewardsBatch2, oraclePrivateKey);
459+
vm.expectRevert(stdError.arithmeticError);
460+
rewards.mintBatchReward(rewardsBatch2, signature2);
461+
}
462+
357463
function createSignature(Rewards.Reward memory reward, uint256 privateKey) internal view returns (bytes memory) {
358464
bytes32 digest = rewards.digestReward(reward);
359465
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);

0 commit comments

Comments
 (0)