Skip to content
Merged
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
147 changes: 74 additions & 73 deletions contracts/contracts/validator-registry/rewards/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading