Skip to content

Commit f80b363

Browse files
committed
feat: add operator receiver functionality for base BGT rewards
- Add operatorReceivers public mapping to route base BGT rewards - Add setOperatorReceiver function for operators to set reward receiver - Add BaseMinted event for tracking base BGT mints - Update BlockRewardController to respect operator receiver settings - Add comprehensive tests for operator receiver functionality
1 parent 5abb80d commit f80b363

File tree

7 files changed

+239
-2
lines changed

7 files changed

+239
-2
lines changed

src/pol/BeaconDeposit.sol

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ contract BeaconDeposit is IBeaconDeposit, ERC165 {
6262
/// @dev The mapping of public keys to operator change requests.
6363
mapping(bytes => QueuedOperator) public queuedOperator;
6464

65+
/// @dev The mapping of operators to their receiver addresses.
66+
mapping(address => address) public operatorReceivers;
67+
6568
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
6669
/* VIEWS */
6770
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
@@ -181,6 +184,21 @@ contract BeaconDeposit is IBeaconDeposit, ERC165 {
181184
emit OperatorUpdated(pubkey, newOperator, oldOperator);
182185
}
183186

187+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
188+
/* OPERATOR RECEIVERS */
189+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
190+
191+
/**
192+
* @notice Sets or updates the receiver address for an operator's base bgt rewards
193+
* @dev Only the operator can set their receiver
194+
* @param receiver The address that will receive base bgt rewards. Set to address(0) to receive rewards directly to operator
195+
*/
196+
function setOperatorReceiver(address receiver) external {
197+
address oldReceiver = operatorReceivers[msg.sender];
198+
operatorReceivers[msg.sender] = receiver;
199+
emit OperatorReceiverUpdated(msg.sender, oldReceiver, receiver);
200+
}
201+
184202
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
185203
/* INTERNAL */
186204
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

src/pol/interfaces/IBeaconDeposit.sol

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ interface IBeaconDeposit is IPOLErrors {
4545
*/
4646
event OperatorUpdated(bytes indexed pubkey, address newOperator, address previousOperator);
4747

48+
/// @notice Emitted when an operator sets or changes their receiver address
49+
/// @param operator The operator address that changed their receiver
50+
/// @param oldReceiver The previous receiver address (zero if first time setting)
51+
/// @param newReceiver The new receiver address (zero if clearing receiver)
52+
event OperatorReceiverUpdated(address indexed operator, address indexed oldReceiver, address indexed newReceiver);
53+
54+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
55+
/* STORAGE */
56+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
57+
58+
/**
59+
* @notice Mapping of operators to their receiver addresses for base rewards
60+
* @dev Returns address(0) if no receiver is set
61+
*/
62+
function operatorReceivers(address) external view returns (address);
63+
4864
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
4965
/* VIEWS */
5066
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
@@ -102,4 +118,11 @@ interface IBeaconDeposit is IPOLErrors {
102118
* @param pubkey The pubkey of the validator.
103119
*/
104120
function acceptOperatorChange(bytes calldata pubkey) external;
121+
122+
/**
123+
* @notice Sets or updates the receiver address for an operator's base rewards
124+
* @dev Only the operator can set their receiver
125+
* @param receiver The address that will receive base rewards. Set to address(0) to receive rewards directly
126+
*/
127+
function setOperatorReceiver(address receiver) external;
105128
}

src/pol/interfaces/IBlockRewardController.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ 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+
5662
/// @notice Returns the constant base rate for BGT.
5763
/// @return The constant base amount of BGT to be minted in the current block.
5864
function baseRate() external view returns (uint256);

src/pol/rewards/BlockRewardController.sol

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,16 @@ contract BlockRewardController is IBlockRewardController, OwnableUpgradeable, UU
235235
// Use the beaconDepositContract to fetch the operator, Its gauranteed to return a valid address.
236236
// Beacon Deposit contract will enforce validators to set an operator.
237237
address operator = beaconDepositContract.getOperator(pubkey);
238-
if (base > 0) bgt.mint(operator, base);
239-
238+
239+
// Check if the operator has set a receiver for their base rewards
240+
address receiver = beaconDepositContract.operatorReceivers(operator);
241+
// If no receiver is set (address(0)), mint to operator directly
242+
if (base > 0) {
243+
address mintTo = receiver == address(0) ? operator : receiver;
244+
bgt.mint(mintTo, base);
245+
emit BaseMinted(operator, mintTo, base);
246+
}
247+
240248
// Mint the scaled rewards BGT for validator reward allocation to the distributor.
241249
if (reward > 0) bgt.mint(distributor, reward);
242250

test/mock/pol/BeaconDepositMock.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity 0.8.26;
33

44
contract BeaconDepositMock {
55
mapping(bytes => address) public pubkeyToOperator;
6+
mapping(address => address) public operatorReceivers;
67

78
function setOperator(bytes memory pubkey, address operator) public {
89
pubkeyToOperator[pubkey] = operator;
@@ -11,4 +12,8 @@ contract BeaconDepositMock {
1112
function getOperator(bytes memory pubkey) public view returns (address) {
1213
return pubkeyToOperator[pubkey];
1314
}
15+
16+
function setOperatorReceiver(address receiver) external {
17+
operatorReceivers[msg.sender] = receiver;
18+
}
1419
}

test/pol/BeaconDeposit.t.sol

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,87 @@ contract BeaconDepositTest is Test {
224224
depositContract.acceptOperatorChange(VALIDATOR_PUBKEY);
225225
}
226226

227+
function test_SetOperatorReceiver() public {
228+
test_Deposit();
229+
address receiver = makeAddr("receiver");
230+
231+
vm.prank(operator);
232+
vm.expectEmit(true, true, true, true);
233+
emit IBeaconDeposit.OperatorReceiverUpdated(operator, address(0), receiver);
234+
depositContract.setOperatorReceiver(receiver);
235+
236+
// Verify receiver is set correctly
237+
assertEq(depositContract.operatorReceivers(operator), receiver);
238+
}
239+
240+
function test_SetOperatorReceiver_Update() public {
241+
test_Deposit();
242+
address receiver1 = makeAddr("receiver1");
243+
address receiver2 = makeAddr("receiver2");
244+
245+
// Set initial receiver
246+
vm.prank(operator);
247+
depositContract.setOperatorReceiver(receiver1);
248+
249+
// Update to new receiver
250+
vm.prank(operator);
251+
vm.expectEmit(true, true, true, true);
252+
emit IBeaconDeposit.OperatorReceiverUpdated(operator, receiver1, receiver2);
253+
depositContract.setOperatorReceiver(receiver2);
254+
255+
assertEq(depositContract.operatorReceivers(operator), receiver2);
256+
}
257+
258+
function test_SetOperatorReceiver_Clear() public {
259+
test_Deposit();
260+
address receiver = makeAddr("receiver");
261+
262+
// Set receiver
263+
vm.prank(operator);
264+
depositContract.setOperatorReceiver(receiver);
265+
266+
// Verify receiver is set correctly
267+
assertEq(depositContract.operatorReceivers(operator), receiver);
268+
269+
// Clear receiver by setting to address(0)
270+
vm.prank(operator);
271+
vm.expectEmit(true, true, true, true);
272+
emit IBeaconDeposit.OperatorReceiverUpdated(operator, receiver, address(0));
273+
depositContract.setOperatorReceiver(address(0));
274+
275+
// Verify receiver is cleared
276+
assertEq(depositContract.operatorReceivers(operator), address(0));
277+
}
278+
279+
function test_GetOperatorReceiver_NonExistent() public {
280+
address nonExistentOperator = makeAddr("nonExistentOperator");
281+
282+
// Should return address(0) for non-existent operator
283+
assertEq(depositContract.operatorReceivers(nonExistentOperator), address(0));
284+
}
285+
286+
function test_SetOperatorReceiver_MultipleOperators() public {
287+
test_Deposit();
288+
289+
// Setup second operator and validator using a different pubkey
290+
bytes memory pubkey2 = abi.encodePacked(bytes32("33"), bytes16("17")); // Different value from _create48Byte()
291+
address operator2 = makeAddr("operator2");
292+
vm.deal(operator2, 10_000 ether);
293+
294+
vm.prank(operator2);
295+
depositContract.deposit{ value: 10_000 ether }(pubkey2, _credential(operator2), _create96Byte(), operator2);
296+
297+
// Set different receivers for first operator but not second operator
298+
address receiver1 = makeAddr("receiver1");
299+
300+
vm.prank(operator);
301+
depositContract.setOperatorReceiver(receiver1);
302+
303+
// Verify receivers are set correctly
304+
assertEq(depositContract.operatorReceivers(operator), receiver1);
305+
assertEq(depositContract.operatorReceivers(operator2), address(0));
306+
}
307+
227308
function testFuzz_AcceptOperatorChange_FailsIfNotEnoughTime(uint256 timeElapsed) public {
228309
timeElapsed = _bound(timeElapsed, 0, 1 days - 1);
229310
testFuzz_RequestOperatorChange(newOperator);

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+
BeaconDepositMock(beaconDepositContract).setOperatorReceiver(receiver);
270+
271+
// Verify receiver is set correctly
272+
assertEq(BeaconDepositMock(beaconDepositContract).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+
BeaconDepositMock(beaconDepositContract).setOperatorReceiver(receiver);
293+
294+
// Then clear it by setting to address(0)
295+
vm.prank(operator);
296+
BeaconDepositMock(beaconDepositContract).setOperatorReceiver(address(0));
297+
298+
// Verify receiver is cleared
299+
assertEq(BeaconDepositMock(beaconDepositContract).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(BeaconDepositMock(beaconDepositContract).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 and validator
335+
address operator2 = makeAddr("operator2");
336+
bytes memory pubkey2 = bytes("validator2");
337+
BeaconDepositMock(beaconDepositContract).setOperator(pubkey2, operator2);
338+
339+
// Setup receivers
340+
address receiver1 = makeAddr("receiver1");
341+
vm.prank(operator);
342+
BeaconDepositMock(beaconDepositContract).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
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)