Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
20 changes: 20 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,12 @@ 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 associated with the provided pubkey can set their receiver
* @param pubkey The validator's public key used to retrieve the operator from the deposit contract
* @param receiver The address that will receive base BGT rewards. Set to address(0) to receive rewards directly
*/
function setOperatorReceiver(bytes calldata pubkey, address receiver) external;
}
39 changes: 37 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,43 @@ 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 associated with the provided pubkey can set their receiver
* @param pubkey The validator's public key used to retrieve the operator from the deposit contract
* @param receiver The address that will receive base BGT rewards. Set to address(0) to receive rewards directly
*/
function setOperatorReceiver(bytes calldata pubkey, address receiver) external {
// Get the operator address from the deposit contract using the provided pubkey
address operator = beaconDepositContract.getOperator(pubkey);

// Ensure that only the operator can set their receiver
if (msg.sender != operator) {
NotOperator.selector.revertWith();
}

address oldReceiver = operatorReceivers[operator];
operatorReceivers[operator] = receiver;
emit OperatorReceiverUpdated(operator, 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(bytes calldata pubkey, address _receiver) external { }
}
119 changes: 119 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(valData.pubkey, 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(valData.pubkey, receiver);

// Then clear it by setting to address(0)
vm.prank(operator);
blockRewardController.setOperatorReceiver(valData.pubkey, 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(valData.pubkey, 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 Expand Up @@ -357,4 +453,27 @@ contract BlockRewardControllerTest is POLTest {
amount = _bound(amount, 1, type(uint128).max / 2);
_helper_ActivateBoost(user, user, pubkey, amount);
}

/// @dev Should fail if not the operator tries to set receiver
function test_SetOperatorReceiver_FailIfNotOperator() public {
// Try to set receiver from a non-operator address
address nonOperator = makeAddr("nonOperator");
vm.prank(nonOperator);
vm.expectRevert(IPOLErrors.NotOperator.selector);
blockRewardController.setOperatorReceiver(valData.pubkey, address(1));
}

/// @dev Should successfully set operator receiver when called by the operator
function test_SetOperatorReceiver_Success() public {
address receiver = makeAddr("receiver");

// Set receiver as the operator
vm.prank(operator);
vm.expectEmit(true, true, true, true);
emit IBlockRewardController.OperatorReceiverUpdated(operator, address(0), receiver);
blockRewardController.setOperatorReceiver(valData.pubkey, receiver);

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