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
16 changes: 3 additions & 13 deletions script/pol/deployment/3_DeployPoL.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,12 @@ contract DeployPoLScript is BaseScript, ConfigPOL, RBAC, AddressBook {
distributor = polDeployer.distributor();
_checkDeploymentAddress("Distributor", address(distributor), _polAddresses.distributor);

// Give roles to the deployer
RBAC.AccountDescription memory deployer = RBAC.AccountDescription({ name: "deployer", addr: msg.sender });

// NOTE: the manager role on the distributor is not assigned to anyone, hence there is no need to revoke it.
RBAC.RoleDescription memory distributorManagerRole = RBAC.RoleDescription({
contractName: "Distributor",
contractAddr: _polAddresses.distributor,
name: "MANAGER_ROLE",
role: distributor.MANAGER_ROLE()
});

_grantRole(distributorManagerRole, deployer);

rewardVaultFactory = polDeployer.rewardVaultFactory();
_checkDeploymentAddress("RewardVaultFactory", address(rewardVaultFactory), _polAddresses.rewardVaultFactory);

// Give roles to the deployer
RBAC.AccountDescription memory deployer = RBAC.AccountDescription({ name: "deployer", addr: msg.sender });

RBAC.RoleDescription memory rewardVaultFactoryManagerRole = RBAC.RoleDescription({
contractName: "RewardVaultFactory",
contractAddr: _polAddresses.rewardVaultFactory,
Expand Down
14 changes: 0 additions & 14 deletions script/pol/deployment/4_TransferPOLOwnership.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@ contract TransferPOLOwnershipScript is RBAC, BaseScript, Storage, AddressBook {
// Placeholder. Change before running the script.
address internal constant NEW_OWNER = address(0); // TIMELOCK_ADDRESS;
address internal constant VAULT_FACTORY_MANAGER = address(0);
address internal constant DISTRIBUTOR_MANAGER = address(0);
address internal constant FEE_COLLECTOR_MANAGER = address(0);

function run() public virtual broadcast {
// Check if the new owner and managers are set
require(NEW_OWNER != address(0), "NEW_OWNER must be set");
require(VAULT_FACTORY_MANAGER != address(0), "VAULT_FACTORY_MANAGER must be set");
require(DISTRIBUTOR_MANAGER != address(0), "DISTRIBUTOR_MANAGER must be set");
require(FEE_COLLECTOR_MANAGER != address(0), "FEE_COLLECTOR_MANAGER must be set");

// create contracts instance from deployed addresses
Expand Down Expand Up @@ -47,9 +45,6 @@ contract TransferPOLOwnershipScript is RBAC, BaseScript, Storage, AddressBook {
RBAC.AccountDescription memory vaultFactoryManager =
RBAC.AccountDescription({ name: "vaultFactoryManager", addr: VAULT_FACTORY_MANAGER });

RBAC.AccountDescription memory distributorManager =
RBAC.AccountDescription({ name: "distributorManager", addr: DISTRIBUTOR_MANAGER });

RBAC.AccountDescription memory deployer = RBAC.AccountDescription({ name: "deployer", addr: msg.sender });

// RewardVaultFactory
Expand Down Expand Up @@ -101,15 +96,6 @@ contract TransferPOLOwnershipScript is RBAC, BaseScript, Storage, AddressBook {
role: distributor.DEFAULT_ADMIN_ROLE()
});

// NOTE: the manager role on the distributor is not assigned to anyone, hence there is no need to revoke it.
RBAC.RoleDescription memory distributorManagerRole = RBAC.RoleDescription({
contractName: "Distributor",
contractAddr: _polAddresses.distributor,
name: "MANAGER_ROLE",
role: distributor.MANAGER_ROLE()
});

_transferRole(distributorManagerRole, deployer, distributorManager);
_transferRole(distributorAdminRole, deployer, governance);
}

Expand Down
21 changes: 0 additions & 21 deletions src/pol/interfaces/IDistributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,6 @@ interface IDistributor is IPOLErrors {
address indexed oldDedicatedEmissionStreamManager, address indexed newDedicatedEmissionStreamManager
);

/**
* @notice Distribute the rewards to the reward allocation receivers.
* @dev Permissionless function to distribute rewards by providing the necessary Merkle proofs. Reverts if the
* proofs are invalid.
* @param nextTimestamp The timestamp of the next beacon block to distribute for. The EIP-4788 Beacon Roots
* contract is queried by this key, returning the parent beacon block root from the next timestamp.
* @param proposerIndex The proposer index of the beacon block. This should be the validator index corresponding
* to the pubkey in the validator registry in the beacon state.
* @param pubkey The validator pubkey of the proposer.
* @param proposerIndexProof The Merkle proof of the proposer index in the beacon block.
* @param pubkeyProof The Merkle proof of the validator pubkey of the proposer in the beacon block.
*/
function distributeFor(
uint64 nextTimestamp,
uint64 proposerIndex,
bytes calldata pubkey,
bytes32[] calldata proposerIndexProof,
bytes32[] calldata pubkeyProof
)
external;

/// @notice Distribute the rewards to the reward allocation receivers according to BRIP-0004.
/// @dev This will be called for block N at the top of block N+1.
/// @dev Only system calls allowed i.e only the execution layer client can call this function.
Expand Down
50 changes: 6 additions & 44 deletions src/pol/rewards/Distributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,12 @@ contract Distributor is
using Utils for bytes4;
using Utils for address;

/// @notice The MANAGER role.
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");

/// @dev Represents 100%. Chosen to be less granular.
uint96 internal constant ONE_HUNDRED_PERCENT = 1e4;

/// @dev Address controlled by the execution layer client and used to call `distributeFor` function.
address private constant SYSTEM_ADDRESS = 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE;

/// @dev Pectra11 hard fork timestamp.
/// @dev Bepolia: 1_754_496_000, 2025-08-06T16:00:00.000Z
/// @dev Mainnet: 1_756_915_200, 2025-09-03T16:00:00.000Z
uint64 private constant PECTRA11_HARD_FORK_TIMESTAMP = 1_756_915_200;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Expand Down Expand Up @@ -97,16 +89,14 @@ contract Distributor is

function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) { }

/// @dev This is necessary to call when the beacon chain hard forks (and specifically the underlying structure of
/// beacon block header is modified).
function setZeroValidatorPubkeyGIndex(uint64 _zeroValidatorPubkeyGIndex) public override onlyRole(MANAGER_ROLE) {
super.setZeroValidatorPubkeyGIndex(_zeroValidatorPubkeyGIndex);
/// @dev Locked — no longer used after Pectra11 hard fork.
function setZeroValidatorPubkeyGIndex(uint64) public pure override {
revert();
}

/// @dev This is necessary to call when the beacon chain hard forks (and specifically the underlying structure of
/// beacon block header is modified).
function setProposerIndexGIndex(uint64 _proposerIndexGIndex) public override onlyRole(MANAGER_ROLE) {
super.setProposerIndexGIndex(_proposerIndexGIndex);
/// @dev Locked — no longer used after Pectra11 hard fork.
function setProposerIndexGIndex(uint64) public pure override {
revert();
}

/// @inheritdoc IDistributor
Expand All @@ -123,34 +113,6 @@ contract Distributor is
dedicatedEmissionStreamManager = IDedicatedEmissionStreamManager(_dedicatedEmissionStreamManager);
}

/// @inheritdoc IDistributor
function distributeFor(
uint64 nextTimestamp,
uint64 proposerIndex,
bytes calldata pubkey,
bytes32[] calldata proposerIndexProof,
bytes32[] calldata pubkeyProof
)
external
nonReentrant
{
// only allow permissionless distribution using proofs till hard fork timestamp
if (nextTimestamp >= PECTRA11_HARD_FORK_TIMESTAMP) {
OnlySystemCallAllowed.selector.revertWith();
}
// Process the timestamp in the history buffer, reverting if already processed.
bytes32 beaconBlockRoot = _processTimestampInBuffer(nextTimestamp);

// Verify the given proposer index is the true proposer index of the beacon block.
_verifyProposerIndexInBeaconBlock(beaconBlockRoot, proposerIndexProof, proposerIndex);

// Verify the given pubkey is of a validator in the beacon block, at the given validator index.
_verifyValidatorPubkeyInBeaconBlock(beaconBlockRoot, pubkeyProof, pubkey, proposerIndex);

// Distribute the rewards to the proposer validator.
_distributeFor(pubkey, nextTimestamp);
}

/// @inheritdoc IDistributor
function distributeFor(bytes calldata pubkey) external onlySystemCall {
_distributeFor(pubkey, uint64(block.timestamp));
Expand Down
21 changes: 2 additions & 19 deletions test/mock/token/ReentrantERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,12 @@ import { IDistributor } from "src/pol/interfaces/IDistributor.sol";
/// that try to reentrancy attack the distributor contract
contract ReentrantERC20 is MockERC20 {
address internal distributor;
uint64 internal timestamp;
uint64 internal proposerIndex;
bytes internal pubkey;
bytes32[] internal proposerIndexProof;
bytes32[] internal pubkeyProof;
bool internal makeExternalCall;

function setDistributeData(
address distributor_,
uint64 timestamp_,
uint64 proposerIndex_,
bytes calldata pubkey_,
bytes32[] calldata proposerIndexProof_,
bytes32[] calldata pubkeyProof_
)
external
{
function setDistributeData(address distributor_, bytes calldata pubkey_) external {
distributor = distributor_;
timestamp = timestamp_;
proposerIndex = proposerIndex_;
pubkey = pubkey_;
proposerIndexProof = proposerIndexProof_;
pubkeyProof = pubkeyProof_;
}

function setMakeExternalCall(bool flag) external {
Expand All @@ -39,7 +22,7 @@ contract ReentrantERC20 is MockERC20 {

function transfer(address recipient, uint256 amount) public override returns (bool) {
if (makeExternalCall) {
IDistributor(distributor).distributeFor(timestamp, proposerIndex, pubkey, proposerIndexProof, pubkeyProof);
IDistributor(distributor).distributeFor(pubkey);
}
return super.transfer(recipient, amount);
}
Expand Down
2 changes: 2 additions & 0 deletions test/pol/BGTIncentiveFeeCollector.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest {
Salt public WBERA_STAKER_VAULT_SALT = Salt({ implementation: 0, proxy: 1 });
Salt public BGT_INCENTIVE_FEE_COLLECTOR_SALT = Salt({ implementation: 0, proxy: 1 });

bytes32 internal managerRole;
bytes32 internal pauserRole;
address internal pauser = makeAddr("pauser");

Expand Down Expand Up @@ -77,6 +78,7 @@ contract BGTIncentiveFeeCollectorTest is DistributorTest {
deal(address(wbera), address(this), 1 ether);

pauserRole = incentiveFeeCollector.PAUSER_ROLE();
managerRole = incentiveFeeCollector.MANAGER_ROLE();
vm.prank(governance);
incentiveFeeCollector.grantRole(managerRole, manager);
vm.prank(manager);
Expand Down
77 changes: 3 additions & 74 deletions test/pol/BeaconRootsHelper.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import { IRewardAllocation } from "src/pol/interfaces/IRewardAllocation.sol";

import "./POL.t.sol";

/// @dev This test is for simulating the whole system against a mock BeraRoots contract.
/// @dev This test sets up the mock BeaconRoots contract and other infrastructure for POL distribution tests.
/// @dev The permissionless distributeFor(timestamp, proofs...) path was removed post-Pectra11.
/// Only the system-call distributeFor(pubkey) path remains.
abstract contract BeaconRootsHelperTest is POLTest {
event AdvancedBlock(uint256 blockNum);

MockHoney internal honey;
RewardVault internal vault;
Mock4788BeaconRoots internal mockBeaconRoots;
Expand Down Expand Up @@ -64,75 +64,4 @@ abstract contract BeaconRootsHelperTest is POLTest {
beraChef.setDefaultRewardAllocation(IRewardAllocation.RewardAllocation(1, weights));
vm.stopPrank();
}

function test_IsTimestampActionable_OutOfBuffer() public virtual {
// Should not be actionable as the timestamp is invalid.
mockBeaconRoots.setIsTimestampValid(false);
assertFalse(distributor.isTimestampActionable(DISTRIBUTE_FOR_TIMESTAMP));
}

function test_IsTimestampActionable_Processing() public virtual {
// Should be actionable as the timestamp is valid in buffer and not processed yet.
mockBeaconRoots.setIsTimestampValid(true);
assertTrue(distributor.isTimestampActionable(DISTRIBUTE_FOR_TIMESTAMP));

// Process the timestamp.
vm.expectEmit(true, false, false, true, address(distributor));
emit BeaconRootsHelper.TimestampProcessed(DISTRIBUTE_FOR_TIMESTAMP);
distributor.distributeFor(
DISTRIBUTE_FOR_TIMESTAMP, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof
);

// Should not be actionable as the timestamp is processed.
assertFalse(distributor.isTimestampActionable(DISTRIBUTE_FOR_TIMESTAMP));
}

/// @dev Should fail if attempted to process a timestamp out of buffer.
function test_ProcessTimestamp_OutOfBuffer() public virtual {
mockBeaconRoots.setIsTimestampValid(false);
vm.expectRevert(BeaconRoots.RootNotFound.selector);
distributor.distributeFor(
DISTRIBUTE_FOR_TIMESTAMP, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof
);
}

function test_ProcessTimestamp_Processing() public virtual {
// Process the valid in buffer, unprocessed timestamp.
mockBeaconRoots.setIsTimestampValid(true);
vm.expectEmit(true, false, false, true, address(distributor));
emit BeaconRootsHelper.TimestampProcessed(DISTRIBUTE_FOR_TIMESTAMP);
distributor.distributeFor(
DISTRIBUTE_FOR_TIMESTAMP, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof
);

// Should fail if attempted to process the timestamp again.
vm.expectRevert(IPOLErrors.TimestampAlreadyProcessed.selector);
distributor.distributeFor(
DISTRIBUTE_FOR_TIMESTAMP, valData.index, valData.pubkey, valData.proposerIndexProof, valData.pubkeyProof
);

// Simulate moving forward, we try now to process a timestamp that should have replaced the same
// `timestamp_idx` in the `_processedTimestampsBuffer` array.
assertTrue(distributor.isTimestampActionable(DISTRIBUTE_FOR_TIMESTAMP + HISTORY_BUFFER_LENGTH));
vm.expectEmit(true, false, false, true, address(distributor));
emit BeaconRootsHelper.TimestampProcessed(DISTRIBUTE_FOR_TIMESTAMP + HISTORY_BUFFER_LENGTH);
distributor.distributeFor(
DISTRIBUTE_FOR_TIMESTAMP + HISTORY_BUFFER_LENGTH,
valData.index,
valData.pubkey,
valData.proposerIndexProof,
valData.pubkeyProof
);

// Should fail if attempted to process the timestamp again.
assertFalse(distributor.isTimestampActionable(DISTRIBUTE_FOR_TIMESTAMP + HISTORY_BUFFER_LENGTH));
vm.expectRevert(IPOLErrors.TimestampAlreadyProcessed.selector);
distributor.distributeFor(
DISTRIBUTE_FOR_TIMESTAMP + HISTORY_BUFFER_LENGTH,
valData.index,
valData.pubkey,
valData.proposerIndexProof,
valData.pubkeyProof
);
}
}
Loading