Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
19 changes: 19 additions & 0 deletions src/pol/interfaces/IBlockRewardController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ interface IBlockRewardController is IPOLErrors {
/// @param rewardRate The amount of BGT minted to the distributor.
event BlockRewardProcessed(bytes indexed pubkey, uint64 nextTimestamp, uint256 baseRate, uint256 rewardRate);

/// @notice Emitted when base BGT is minted to either the operator or their receiver
/// @param operator The operator address that earned the base rewards
/// @param receiver The address that received the base rewards (operator or their set receiver)
/// @param amount The amount of base BGT minted
event BaseMinted(address indexed operator, address indexed receiver, uint256 indexed amount);

/// @notice Emitted when an operator sets or changes their receiver address
/// @param operator The operator address that changed their receiver
/// @param oldReceiver The previous receiver address (zero if first time setting)
/// @param newReceiver The new receiver address (zero if clearing receiver)
event OperatorReceiverUpdated(address indexed operator, address indexed oldReceiver, address indexed newReceiver);

/// @notice Returns the constant base rate for BGT.
/// @return The constant base amount of BGT to be minted in the current block.
function baseRate() external view returns (uint256);
Expand Down Expand Up @@ -152,4 +164,11 @@ interface IBlockRewardController is IPOLErrors {
* @param _distributor The new distributor contract.
*/
function setDistributor(address _distributor) external;

/**
* @notice Sets or updates the receiver address for an operator's base BGT rewards
* @dev Only the operator can set their receiver
* @param receiver The address that will receive base BGT rewards. Set to address(0) to receive rewards directly
*/
function setOperatorReceiver(address receiver) external;
}
30 changes: 28 additions & 2 deletions src/pol/rewards/BlockRewardController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ contract BlockRewardController is IBlockRewardController, OwnableUpgradeable, UU
/// @notice The reward convexity param in the function, determines how fast it converges to its max, 18 dec.
int256 public rewardConvexity;

/// @notice The mapping of operators to their receiver addresses for base rewards
mapping(address => address) public operatorReceivers;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
Expand Down Expand Up @@ -235,11 +238,34 @@ contract BlockRewardController is IBlockRewardController, OwnableUpgradeable, UU
// Use the beaconDepositContract to fetch the operator, Its gauranteed to return a valid address.
// Beacon Deposit contract will enforce validators to set an operator.
address operator = beaconDepositContract.getOperator(pubkey);
if (base > 0) bgt.mint(operator, base);


// Check if the operator has set a receiver for their base rewards
address receiver = operatorReceivers[operator];
// If no receiver is set (address(0)), mint to operator directly
if (base > 0) {
address mintTo = receiver == address(0) ? operator : receiver;
bgt.mint(mintTo, base);
emit BaseMinted(operator, mintTo, base);
}

// Mint the scaled rewards BGT for validator reward allocation to the distributor.
if (reward > 0) bgt.mint(distributor, reward);

return reward;
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* OPERATOR RECEIVERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/**
* @notice Sets or updates the receiver address for an operator's base BGT rewards
* @dev Only the operator can set their receiver
* @param receiver The address that will receive base BGT rewards. Set to address(0) to receive rewards directly
*/
function setOperatorReceiver(address receiver) external {
address oldReceiver = operatorReceivers[msg.sender];
Copy link
Copy Markdown
Collaborator

@sj448 sj448 Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would suggest to take pubkey too as an input to get operator address from deposit contract and put an explicit check of msg.sender==operator to avoid spamming of this function.

Copy link
Copy Markdown
Author

@ZERGUCCI ZERGUCCI Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intentionally set it up so that you can only set your own operator receiver (sets directly to msg.sender) without the need for making an external call to the distributor to increase security in the event the distributor contract somehow gets exploited and for general gas efficiency.

I understand your point of people being able to spam it though, what do you see as the motive of people doing that?

Copy link
Copy Markdown
Author

@ZERGUCCI ZERGUCCI Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The potential spammer in question would cause no harm as they are setting their own operator receiver and the gas cost is a natural deterrent to this behavior. I can add the check if you'd like though

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've implemented the suggested changes as I believe you might be one of the Berachain developers, though I'm not entirely certain.

Copy link
Copy Markdown
Collaborator

@sj448 sj448 Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but we’ll keep the PR open because this change will mainly go live with POL v2. Since we’re planning to use EIP‑2537 for v2, the current implementation of this feature might no longer be relevant at that point.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. How long do you think roughly until POL V2 goes live?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont have any estimate on this yet, we will update you once things are in motion

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay sounds good, I will familiarize myself with EIP-2537 in the meantime to better understand the change it will have in this context.

operatorReceivers[msg.sender] = receiver;
emit OperatorReceiverUpdated(msg.sender, oldReceiver, receiver);
}
}
3 changes: 3 additions & 0 deletions test/mock/pol/NoopBlockRewardController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,7 @@ contract NoopBlockRewardController is IBlockRewardController {

/// @inheritdoc IBlockRewardController
function setDistributor(address _distributor) external { }

/// @inheritdoc IBlockRewardController
function setOperatorReceiver(address _receiver) external { }
}
96 changes: 96 additions & 0 deletions test/pol/BlockRewardController.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,102 @@ contract BlockRewardControllerTest is POLTest {
assertApproxEqAbs(reward, expected, maxDelta);
}

/// @dev Should process rewards with operator receiver set
function test_ProcessRewardsWithReceiver() public {
test_SetDistributor();
test_SetBaseRate();

address receiver = makeAddr("receiver");
// Set receiver for operator
vm.prank(operator);
blockRewardController.setOperatorReceiver(receiver);

// Verify receiver is set correctly
assertEq(blockRewardController.operatorReceivers(operator), receiver);

// Process rewards - should mint base rewards to receiver instead of operator
vm.prank(address(distributor));
vm.expectEmit(true, true, true, true);
emit IBlockRewardController.BlockRewardProcessed(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, 1 ether, 0);

// expect call to mint BGT to the receiver instead of operator
vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (receiver, 1.0 ether)));
blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true);
}

/// @dev Should process rewards with operator receiver cleared
function test_ProcessRewardsWithReceiverCleared() public {
test_SetDistributor();
test_SetBaseRate();

address receiver = makeAddr("receiver");
// First set a receiver
vm.prank(operator);
blockRewardController.setOperatorReceiver(receiver);

// Then clear it by setting to address(0)
vm.prank(operator);
blockRewardController.setOperatorReceiver(address(0));

// Verify receiver is cleared
assertEq(blockRewardController.operatorReceivers(operator), address(0));

// Process rewards - should mint base rewards to operator since receiver is cleared
vm.prank(address(distributor));
vm.expectEmit(true, true, true, true);
emit IBlockRewardController.BlockRewardProcessed(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, 1 ether, 0);

// expect call to mint BGT to the operator since receiver is cleared
vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (operator, 1.0 ether)));
blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true);
}

/// @dev Should process rewards with no receiver set (default case)
function test_ProcessRewardsWithNoReceiver() public {
test_SetDistributor();
test_SetBaseRate();

// Verify no receiver is set
assertEq(blockRewardController.operatorReceivers(operator), address(0));

// Process rewards - should mint base rewards to operator since no receiver set
vm.prank(address(distributor));
vm.expectEmit(true, true, true, true);
emit IBlockRewardController.BlockRewardProcessed(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, 1 ether, 0);

// expect call to mint BGT to the operator since no receiver set
vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (operator, 1.0 ether)));
blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true);
}

/// @dev Should process rewards correctly when multiple operators have different receiver settings
function test_ProcessRewardsMultipleOperators() public {
test_SetDistributor();
test_SetBaseRate();

// Setup second operator with mock BeaconDeposit
address operator2 = makeAddr("operator2");
bytes memory pubkey2 = bytes("validator2");
BeaconDepositMock(beaconDepositContract).setOperator(pubkey2, operator2);

// Setup receivers in BlockRewardController
address receiver1 = makeAddr("receiver1");
vm.prank(operator);
blockRewardController.setOperatorReceiver(receiver1);

// Don't set receiver for operator2

// Process rewards for first operator - should go to receiver1
vm.prank(address(distributor));
vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (receiver1, 1.0 ether)));
blockRewardController.processRewards(valData.pubkey, DISTRIBUTE_FOR_TIMESTAMP, true);

// Process rewards for second operator - should go to operator2 directly
vm.prank(address(distributor));
vm.expectCall(address(bgt), abi.encodeCall(IBGT.mint, (operator2, 1.0 ether)));
blockRewardController.processRewards(pubkey2, DISTRIBUTE_FOR_TIMESTAMP, true);
}

/// @dev Should bound compute rewards correctly to its theoretical limits
function testFuzz_ComputeRewards(
uint256 boostPower,
Expand Down