diff --git a/contracts/contracts/validator-registry/rewards/README.md b/contracts/contracts/validator-registry/rewards/README.md index 6c8fe4219..48c3debbf 100644 --- a/contracts/contracts/validator-registry/rewards/README.md +++ b/contracts/contracts/validator-registry/rewards/README.md @@ -20,123 +20,124 @@ IBlockRewardManager(brm).payProposer{value: reward}(feeRecipient); ``` +# Reward Distributor — Operator Guide -# Reward Distributor — Overview (ETH-focused) +The `RewardDistributor` contract is used to **receive, track, and distribute additional ETH or token rewards** earned by operators with active validators. -For further details, see the Reward Distributor Design Doc: https://www.notion.so/primev/RewardDistributor-Design-2696865efd6f80b2a4f0e6b8fc3ab0c4 +When operators earn extra rewards (for example, from mev-commit participation or other incentive programs), those rewards are **granted to this contract** and then **claimed by operators to their specified fee recipients**. -`RewardDistributor` tracks and pays **ETH stipends** to operator-defined recipients based on validator-key participation. Operators map their validator BLS pubkeys to payout addresses (“recipients”) and may authorize delegates to claim on their behalf. - -> **Note:** The contract also supports granting **ERC20 token rewards** (single-token per `tokenID`) and will utilize this in the future. In the near term, **ETH (tokenID `0`)** is the primary path. + +> Rewards are granted on-chain to the `RewardDistributor`, tracked per operator and recipient, and later claimed by operators (or their delegates) to the correct payout addresses. --- -## Setting recipients +### Core Contract Functionality -- **Global default (applies to all keys unless overridden):** - `setOperatorGlobalOverride(address recipient)` - Sets a default recipient for the operator’s keys. +- Hold **additional ETH or token rewards** earned by operators +- Allow operators to **define where rewards should be paid** +- Ensure rewards are **isolated per operator and recipient** +- Support **delegated claiming** if operators want others to claim on their behalf -- **Per-key override (takes precedence over the default):** - `overrideRecipientByPubkey(bytes[] pubkeys, address recipient)` - Assigns a recipient for one or more BLS pubkeys (each must be 48 bytes). - **Precedence:** per-key override → global override → fallback to the operator address. +The contract does **not** automatically push funds. Operators (or authorized delegates) explicitly claim rewards when ready. -- **Resolve the active recipient for a key:** - `getKeyRecipient(address operator, bytes pubkey) → address` - Returns the payout address considering per-key overrides, global override, or operator fallback. +--- -- **Migrate unclaimed accruals between recipients (for the calling operator):** - `migrateExistingRewards(address from, address to, uint256 tokenID)` - Moves **unclaimed** rewards for `(msg.sender, from, tokenID)` into `(msg.sender, to, tokenID)`. - Use `tokenID = 0` for ETH; future token IDs correspond to configured ERC20s. +### Core roles/concepts ---- +- **Operator** + The address that controls one or more validators and interacts with the contract. -## Delegation (optional) +- **Recipient** + The address that ultimately receives rewards (often the validator’s fee recipient or another payout address). -- **Authorize a delegate to claim for a specific recipient:** - `setClaimDelegate(address delegate, address recipient, bool status)` - When `status = true`, `delegate` may claim for `(operator, recipient)`; set `false` to revoke. +- **Validator pubkey mapping** + Operators can map validator BLS pubkeys to specific recipients. -- **Delegate claim (on behalf of operator):** - `claimOnbehalfOfOperator(address operator, address payable[] recipients, uint256 tokenID)` - Delegate must be authorized **per recipient** by that operator. ---- +Rewards are tracked separately for each: -## Rewards & claiming (ETH-first) +(operator, recipient, tokenID) -1. **Accrual off-chain; batched grants on-chain.** - A RewardManager service monitors blocks won by mev-commit–registered validators, resolves recipients via `getKeyRecipient`, and tallies `(operator, recipient)` off-chain. At period end, it submits **batched grants**: +--- - - **ETH grants (primary path):** - `grantETHRewards(Distribution[] distributions)` - The transaction `msg.value` must equal the sum of `distributions[i].amount`. +## Setting reward recipients - - **Token grants (future use):** - `grantTokenRewards(Distribution[] distributions, uint256 tokenID)` - Pulls tokens from `msg.sender` via `transferFrom`. Requires prior `approve`. +>If a recipient is not set, rewards earned by a validator are granted to the validator's operator. - A `Distribution` item packs: `{operator, recipient, amount}`. +### 1. Set a global default recipient -2. **Claim by operator (pull-to-recipient):** - `claimRewards(address payable[] recipients, uint256 tokenID)` - For each recipient listed, transfers the **full pending** amount for that `(operator, recipient, tokenID)` bucket to the recipient. - - ETH: use `tokenID = 0`. - - Tokens: use the configured nonzero `tokenID` (future use). +Applies to all validator keys unless overridden: -3. **Get pending rewards:** - `getPendingRewards(address operator, address recipient, uint256 tokenID) → uint128` - Computed as `accrued - claimed` for that bucket. +setOperatorGlobalOverride(address recipient) -> **Isolation guarantee:** Balances are **strictly partitioned** by `(operator, recipient, tokenID)`. Multiple operators can share a recipient, but each operator can only claim their own bucket for that recipient. +If no per-key override exists, rewards for the operator’s validators will go here. --- -## Reclaim by owner (administrative) +### 2. (Optional) Set per-validator overrides -- **Owner can reclaim accrued rewards to itself:** - `reclaimStipendsToOwner(address[] operators, address[] recipients, uint256 tokenID)` - Sums claimable amounts across the provided pairs, transfers them to the **owner**, and zeroes the accruals. - Requirements: arrays must be equal length; total claimable must be nonzero. +If desired, an operator can set per-key recipients which override the operator's global default recipient: + +overrideRecipientByPubkey(bytes[] pubkeys, address recipient) + +- Each pubkey must be exactly **48 bytes** +- Can be used for one or many pubkeys +- Takes precedence over the global override --- -## Access control & safety +### 3. How recipients are resolved + +For a given validator key, the active recipient is resolved as: + +1. Per-key override +2. Global override +3. Operator address (fallback) -- **Grant permissions:** Only the **owner** or the **reward manager** may call `grantETHRewards` / `grantTokenRewards`. -- **Pause:** Owner can `pause()`/`unpause()` to block mutating endpoints (grants, claims, and—if configured—delegation/override changes). -- **Zero-address checks & input validation:** Functions validate parameters (e.g., nonzero addresses, 48-byte pubkeys, registered token IDs, consistent array lengths). +You can query this directly: + +getKeyRecipient(address operator, bytes pubkey) → address --- -## Events (key ones) +## Receiving rewards -- **ETH grants:** `ETHGranted(address indexed operator, address indexed recipient, uint256 indexed amount)` -- **ETH claims:** `ETHRewardsClaimed(address indexed operator, address indexed recipient, uint256 indexed amount)` -- **Operator Reward Migrations** `RewardsMigrated(uint256 tokenID, address indexed operator, address indexed from, address indexed to, uint128 amount)` +Rewards are **granted to the contract**, not sent directly to recipients. -- **(Future) token grants:** Implementation emits analogous token-grant events and batch totals where applicable. -- **Admin updates:** Events are emitted for reward manager and token registration changes. +- Grants are performed by an authorized reward manager or the contract owner +- Rewards correspond to operator activity (e.g., active validators) +- Operators do **not** need to take action to receive grants -> Event names above match the interface (`IRewardDistributor`) for ETH. Token event names may vary depending on your current implementation; update this section if you finalize them. +Once granted, rewards accumulate under the operator until claimed. --- -## Typical ETH stipend flow +## Claiming rewards + +### Claim as the operator + +To transfer all pending rewards to one or more recipients: + +claimRewards(address payable[] recipients, uint256 tokenID) -1. Operator sets a **global default** recipient and optional **per-key overrides**. -2. Over the period, off-chain tally adds up ETH stipends per `(operator, recipient)`. -3. After the period, RewardManager calls `grantETHRewards([...])` with the **consolidated totals**. -4. Operator (or an authorized delegate) calls `claimRewards([recipients], 0)` to transfer ETH to each listed recipient. +- ETH rewards: `tokenID = 0` +- Token rewards: use the configured nonzero `tokenID` +- Transfers the **full pending amount** for each listed recipient --- -## Notes on future token support +## Delegating claims (optional) -- **Registration:** Owner maps an ERC20 to a nonzero `tokenID` via `setRewardToken(address token, uint256 tokenID)`. -- **Grants:** Use `grantTokenRewards(distributions, tokenID)`; caller must hold tokens and `approve` the distributor. -- **Claims:** Same claim APIs as ETH but pass the nonzero `tokenID`. Buckets remain isolated per `tokenID`. +Operators may allow another address to claim rewards on their behalf. + +setClaimDelegate(address delegate, address recipient, bool status) + +Delegation is configured **per recipient**. --- + +## Safety and guarantees + +- Rewards are **strictly isolated** by `(operator, recipient, tokenID)` +- Operators can only claim their own rewards +- Multiple operators may use the same recipient address safely