Skip to content

Commit 238759e

Browse files
committed
feat: add operator receiver functionality to BlockRewardController
This commit adds the ability for operators to designate a receiver address for their base BGT rewards. The functionality is implemented directly in BlockRewardController to avoid requiring hard fork changes to BeaconDeposit.
1 parent 5abb80d commit 238759e

File tree

4 files changed

+146
-2
lines changed

4 files changed

+146
-2
lines changed

src/pol/interfaces/IBlockRewardController.sol

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ interface IBlockRewardController is IPOLErrors {
5353
/// @param rewardRate The amount of BGT minted to the distributor.
5454
event BlockRewardProcessed(bytes indexed pubkey, uint64 nextTimestamp, uint256 baseRate, uint256 rewardRate);
5555

56+
/// @notice Emitted when base BGT is minted to either the operator or their receiver
57+
/// @param operator The operator address that earned the base rewards
58+
/// @param receiver The address that received the base rewards (operator or their set receiver)
59+
/// @param amount The amount of base BGT minted
60+
event BaseMinted(address indexed operator, address indexed receiver, uint256 indexed amount);
61+
62+
/// @notice Emitted when an operator sets or changes their receiver address
63+
/// @param operator The operator address that changed their receiver
64+
/// @param oldReceiver The previous receiver address (zero if first time setting)
65+
/// @param newReceiver The new receiver address (zero if clearing receiver)
66+
event OperatorReceiverUpdated(address indexed operator, address indexed oldReceiver, address indexed newReceiver);
67+
5668
/// @notice Returns the constant base rate for BGT.
5769
/// @return The constant base amount of BGT to be minted in the current block.
5870
function baseRate() external view returns (uint256);
@@ -152,4 +164,11 @@ interface IBlockRewardController is IPOLErrors {
152164
* @param _distributor The new distributor contract.
153165
*/
154166
function setDistributor(address _distributor) external;
167+
168+
/**
169+
* @notice Sets or updates the receiver address for an operator's base BGT rewards
170+
* @dev Only the operator can set their receiver
171+
* @param receiver The address that will receive base BGT rewards. Set to address(0) to receive rewards directly
172+
*/
173+
function setOperatorReceiver(address receiver) external;
155174
}

src/pol/rewards/BlockRewardController.sol

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ contract BlockRewardController is IBlockRewardController, OwnableUpgradeable, UU
6363
/// @notice The reward convexity param in the function, determines how fast it converges to its max, 18 dec.
6464
int256 public rewardConvexity;
6565

66+
/// @notice The mapping of operators to their receiver addresses for base rewards
67+
mapping(address => address) public operatorReceivers;
68+
6669
/// @custom:oz-upgrades-unsafe-allow constructor
6770
constructor() {
6871
_disableInitializers();
@@ -235,11 +238,34 @@ contract BlockRewardController is IBlockRewardController, OwnableUpgradeable, UU
235238
// Use the beaconDepositContract to fetch the operator, Its gauranteed to return a valid address.
236239
// Beacon Deposit contract will enforce validators to set an operator.
237240
address operator = beaconDepositContract.getOperator(pubkey);
238-
if (base > 0) bgt.mint(operator, base);
239-
241+
242+
// Check if the operator has set a receiver for their base rewards
243+
address receiver = operatorReceivers[operator];
244+
// If no receiver is set (address(0)), mint to operator directly
245+
if (base > 0) {
246+
address mintTo = receiver == address(0) ? operator : receiver;
247+
bgt.mint(mintTo, base);
248+
emit BaseMinted(operator, mintTo, base);
249+
}
250+
240251
// Mint the scaled rewards BGT for validator reward allocation to the distributor.
241252
if (reward > 0) bgt.mint(distributor, reward);
242253

243254
return reward;
244255
}
256+
257+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
258+
/* OPERATOR RECEIVERS */
259+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
260+
261+
/**
262+
* @notice Sets or updates the receiver address for an operator's base BGT rewards
263+
* @dev Only the operator can set their receiver
264+
* @param receiver The address that will receive base BGT rewards. Set to address(0) to receive rewards directly
265+
*/
266+
function setOperatorReceiver(address receiver) external {
267+
address oldReceiver = operatorReceivers[msg.sender];
268+
operatorReceivers[msg.sender] = receiver;
269+
emit OperatorReceiverUpdated(msg.sender, oldReceiver, receiver);
270+
}
245271
}

test/mock/pol/NoopBlockRewardController.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,7 @@ contract NoopBlockRewardController is IBlockRewardController {
6262

6363
/// @inheritdoc IBlockRewardController
6464
function setDistributor(address _distributor) external { }
65+
66+
/// @inheritdoc IBlockRewardController
67+
function setOperatorReceiver(address _receiver) external { }
6568
}

test/pol/BlockRewardController.t.sol

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,102 @@ contract BlockRewardControllerTest is POLTest {
258258
assertApproxEqAbs(reward, expected, maxDelta);
259259
}
260260

261+
/// @dev Should process rewards with operator receiver set
262+
function test_ProcessRewardsWithReceiver() public {
263+
test_SetDistributor();
264+
test_SetBaseRate();
265+
266+
address receiver = makeAddr("receiver");
267+
// Set receiver for operator
268+
vm.prank(operator);
269+
blockRewardController.setOperatorReceiver(receiver);
270+
271+
// Verify receiver is set correctly
272+
assertEq(blockRewardController.operatorReceivers(operator), receiver);
273+
274+
// Process rewards - should mint base rewards to receiver instead of operator
275+
vm.prank(address(distributor));
276+
vm.expectEmit(true, true, true, true);
277+
emit IBlockRewardController.BlockRewardProcessed(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, 1 ether, 0);
278+
279+
// expect call to mint BGT to the receiver instead of operator
280+
vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (receiver, 1.0 ether)));
281+
blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true);
282+
}
283+
284+
/// @dev Should process rewards with operator receiver cleared
285+
function test_ProcessRewardsWithReceiverCleared() public {
286+
test_SetDistributor();
287+
test_SetBaseRate();
288+
289+
address receiver = makeAddr("receiver");
290+
// First set a receiver
291+
vm.prank(operator);
292+
blockRewardController.setOperatorReceiver(receiver);
293+
294+
// Then clear it by setting to address(0)
295+
vm.prank(operator);
296+
blockRewardController.setOperatorReceiver(address(0));
297+
298+
// Verify receiver is cleared
299+
assertEq(blockRewardController.operatorReceivers(operator), address(0));
300+
301+
// Process rewards - should mint base rewards to operator since receiver is cleared
302+
vm.prank(address(distributor));
303+
vm.expectEmit(true, true, true, true);
304+
emit IBlockRewardController.BlockRewardProcessed(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, 1 ether, 0);
305+
306+
// expect call to mint BGT to the operator since receiver is cleared
307+
vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (operator, 1.0 ether)));
308+
blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true);
309+
}
310+
311+
/// @dev Should process rewards with no receiver set (default case)
312+
function test_ProcessRewardsWithNoReceiver() public {
313+
test_SetDistributor();
314+
test_SetBaseRate();
315+
316+
// Verify no receiver is set
317+
assertEq(blockRewardController.operatorReceivers(operator), address(0));
318+
319+
// Process rewards - should mint base rewards to operator since no receiver set
320+
vm.prank(address(distributor));
321+
vm.expectEmit(true, true, true, true);
322+
emit IBlockRewardController.BlockRewardProcessed(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, 1 ether, 0);
323+
324+
// expect call to mint BGT to the operator since no receiver set
325+
vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (operator, 1.0 ether)));
326+
blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true);
327+
}
328+
329+
/// @dev Should process rewards correctly when multiple operators have different receiver settings
330+
function test_ProcessRewardsMultipleOperators() public {
331+
test_SetDistributor();
332+
test_SetBaseRate();
333+
334+
// Setup second operator with mock BeaconDeposit
335+
address operator2 = makeAddr("operator2");
336+
bytes memory pubkey2 = bytes("validator2");
337+
BeaconDepositMock(beaconDepositContract).setOperator(pubkey2, operator2);
338+
339+
// Setup receivers in BlockRewardController
340+
address receiver1 = makeAddr("receiver1");
341+
vm.prank(operator);
342+
blockRewardController.setOperatorReceiver(receiver1);
343+
344+
// Don't set receiver for operator2
345+
346+
// Process rewards for first operator - should go to receiver1
347+
vm.prank(address(distributor));
348+
vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (receiver1, 1.0 ether)));
349+
blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true);
350+
351+
// Process rewards for second operator - should go to operator2 directly
352+
vm.prank(address(distributor));
353+
vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (operator2, 1.0 ether)));
354+
blockRewardController.processRewards(pubkey2, DISTRIBUTE_FOR_TIMESTAMP, true);
355+
}
356+
261357
/// @dev Should bound compute rewards correctly to its theoretical limits
262358
function testFuzz_ComputeRewards(
263359
uint256 boostPower,

0 commit comments

Comments
 (0)