From 238759e737175b10fd601d5801eb6678a9ff6959 Mon Sep 17 00:00:00 2001 From: Zergucci <38669066+ZERGUCCI@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:21:20 -0800 Subject: [PATCH 1/2] 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. --- src/pol/interfaces/IBlockRewardController.sol | 19 ++++ src/pol/rewards/BlockRewardController.sol | 30 +++++- test/mock/pol/NoopBlockRewardController.sol | 3 + test/pol/BlockRewardController.t.sol | 96 +++++++++++++++++++ 4 files changed, 146 insertions(+), 2 deletions(-) diff --git a/src/pol/interfaces/IBlockRewardController.sol b/src/pol/interfaces/IBlockRewardController.sol index 38a03a2..658e6f2 100644 --- a/src/pol/interfaces/IBlockRewardController.sol +++ b/src/pol/interfaces/IBlockRewardController.sol @@ -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); @@ -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; } diff --git a/src/pol/rewards/BlockRewardController.sol b/src/pol/rewards/BlockRewardController.sol index 3d9a161..f5067dc 100644 --- a/src/pol/rewards/BlockRewardController.sol +++ b/src/pol/rewards/BlockRewardController.sol @@ -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(); @@ -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]; + operatorReceivers[msg.sender] = receiver; + emit OperatorReceiverUpdated(msg.sender, oldReceiver, receiver); + } } diff --git a/test/mock/pol/NoopBlockRewardController.sol b/test/mock/pol/NoopBlockRewardController.sol index 313b166..efacc89 100644 --- a/test/mock/pol/NoopBlockRewardController.sol +++ b/test/mock/pol/NoopBlockRewardController.sol @@ -62,4 +62,7 @@ contract NoopBlockRewardController is IBlockRewardController { /// @inheritdoc IBlockRewardController function setDistributor(address _distributor) external { } + + /// @inheritdoc IBlockRewardController + function setOperatorReceiver(address _receiver) external { } } diff --git a/test/pol/BlockRewardController.t.sol b/test/pol/BlockRewardController.t.sol index 8277eb9..02ffe71 100644 --- a/test/pol/BlockRewardController.t.sol +++ b/test/pol/BlockRewardController.t.sol @@ -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, From a015dd2a05e4503a2b0756bbad2b8fe27f301454 Mon Sep 17 00:00:00 2001 From: Zergucci <38669066+ZERGUCCI@users.noreply.github.com> Date: Thu, 27 Feb 2025 22:57:13 -0800 Subject: [PATCH 2/2] fix: reduce potential setOperatorReceiver spam by requiring valid pubkey ownership --- src/pol/interfaces/IBlockRewardController.sol | 5 +-- src/pol/rewards/BlockRewardController.sol | 19 +++++++++--- test/mock/pol/NoopBlockRewardController.sol | 2 +- test/pol/BlockRewardController.t.sol | 31 ++++++++++++++++--- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/pol/interfaces/IBlockRewardController.sol b/src/pol/interfaces/IBlockRewardController.sol index 658e6f2..848d704 100644 --- a/src/pol/interfaces/IBlockRewardController.sol +++ b/src/pol/interfaces/IBlockRewardController.sol @@ -167,8 +167,9 @@ interface IBlockRewardController is IPOLErrors { /** * @notice Sets or updates the receiver address for an operator's base BGT rewards - * @dev Only the operator can set their receiver + * @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(address receiver) external; + function setOperatorReceiver(bytes calldata pubkey, address receiver) external; } diff --git a/src/pol/rewards/BlockRewardController.sol b/src/pol/rewards/BlockRewardController.sol index f5067dc..a897a59 100644 --- a/src/pol/rewards/BlockRewardController.sol +++ b/src/pol/rewards/BlockRewardController.sol @@ -260,12 +260,21 @@ contract BlockRewardController is IBlockRewardController, OwnableUpgradeable, UU /** * @notice Sets or updates the receiver address for an operator's base BGT rewards - * @dev Only the operator can set their receiver + * @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(address receiver) external { - address oldReceiver = operatorReceivers[msg.sender]; - operatorReceivers[msg.sender] = receiver; - emit OperatorReceiverUpdated(msg.sender, oldReceiver, receiver); + 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); } } diff --git a/test/mock/pol/NoopBlockRewardController.sol b/test/mock/pol/NoopBlockRewardController.sol index efacc89..629277f 100644 --- a/test/mock/pol/NoopBlockRewardController.sol +++ b/test/mock/pol/NoopBlockRewardController.sol @@ -64,5 +64,5 @@ contract NoopBlockRewardController is IBlockRewardController { function setDistributor(address _distributor) external { } /// @inheritdoc IBlockRewardController - function setOperatorReceiver(address _receiver) external { } + function setOperatorReceiver(bytes calldata pubkey, address _receiver) external { } } diff --git a/test/pol/BlockRewardController.t.sol b/test/pol/BlockRewardController.t.sol index 02ffe71..f5b5462 100644 --- a/test/pol/BlockRewardController.t.sol +++ b/test/pol/BlockRewardController.t.sol @@ -266,7 +266,7 @@ contract BlockRewardControllerTest is POLTest { address receiver = makeAddr("receiver"); // Set receiver for operator vm.prank(operator); - blockRewardController.setOperatorReceiver(receiver); + blockRewardController.setOperatorReceiver(valData.pubkey, receiver); // Verify receiver is set correctly assertEq(blockRewardController.operatorReceivers(operator), receiver); @@ -289,11 +289,11 @@ contract BlockRewardControllerTest is POLTest { address receiver = makeAddr("receiver"); // First set a receiver vm.prank(operator); - blockRewardController.setOperatorReceiver(receiver); + blockRewardController.setOperatorReceiver(valData.pubkey, receiver); // Then clear it by setting to address(0) vm.prank(operator); - blockRewardController.setOperatorReceiver(address(0)); + blockRewardController.setOperatorReceiver(valData.pubkey, address(0)); // Verify receiver is cleared assertEq(blockRewardController.operatorReceivers(operator), address(0)); @@ -339,7 +339,7 @@ contract BlockRewardControllerTest is POLTest { // Setup receivers in BlockRewardController address receiver1 = makeAddr("receiver1"); vm.prank(operator); - blockRewardController.setOperatorReceiver(receiver1); + blockRewardController.setOperatorReceiver(valData.pubkey, receiver1); // Don't set receiver for operator2 @@ -453,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); + } }