From 30a6c69a4de4ccaf0acc279f1a8db95ea3cf26e5 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Thu, 2 Jul 2026 12:23:00 +0200 Subject: [PATCH 1/4] docs: add Delegated Reward Calling draft LIP Add a draft LIP for delegating the reward() call to a separate low-privilege address so an orchestrator's stake-holding wallet can stay cold. Based on the implementation in livepeer/protocol#648. --- LIPs/LIP-delegated-reward-calling.md | 88 ++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 LIPs/LIP-delegated-reward-calling.md diff --git a/LIPs/LIP-delegated-reward-calling.md b/LIPs/LIP-delegated-reward-calling.md new file mode 100644 index 0000000..e3e26c4 --- /dev/null +++ b/LIPs/LIP-delegated-reward-calling.md @@ -0,0 +1,88 @@ +--- +lip: 108 +title: Delegated Reward Calling +author: Rick Staa (@rickstaa), (@), Nico Vergauwen (@kyriediculous) +type: Standard Track +status: Draft +created: 2026-07-02 +discussions-to: +--- + +## Abstract + +This proposal adds a delegated **reward caller** to the `BondingManager`, allowing an orchestrator to authorize a separate address to call `reward()` on its behalf. + +Today, orchestrators must call `reward()` once per round from the same wallet that holds their self-bonded LPT and accrued fees. Automating this call forces that high-value wallet to stay online and unlocked (a "hot wallet"), exposing stake, fees, and governance voting power to compromise, and making multisig orchestrators impractical. + +The reward caller is scoped to `reward()` **only** — it cannot move, unbond, or withdraw funds, change configuration, vote, or redirect rewards, which always accrue to and are checkpointed for the orchestrator. This lets orchestrators keep their high-value wallet cold/offline while the reward caller performs the routine call, and unblocks multisig orchestrators. + +## Motivation + +Orchestrators must call `reward()` once per round to claim inflationary rewards, and the call must originate from the same wallet that holds their self-bonded LPT and accrued fees. Automating this requires that wallet to remain online and unlocked (a hot wallet), which is a standing security risk: + +- That wallet also controls the orchestrator's self-bonded stake, fees, commission, and unbonding, and casts its governance votes. If compromised, funds can be stolen, governance power abused, and recovery forces a disruptive wallet migration in which delegators must re-delegate. +- It also makes multisig orchestrators impractical, because the reward call must be signed every round, which a multisig cannot reasonably automate. Ticket redemption can already be delegated to a separate address, so `reward()` is the only remaining call that forces the main key online. + +Orchestrators can partially mitigate the hot-wallet risk through operational practices, but none eliminate it or enable multisig operation. + +This proposal therefore enables an orchestrator to delegate the reward call to a separate, genuinely low-privilege address (a reward caller) so the high-value wallet can stay cold, leaving all other protocol behavior unchanged. + +## Specification + +This proposal adds a per-orchestrator reward caller to `BondingManager` (interface in `IBondingManager`): + +```solidity +// New event (added to IBondingManager) +event RewardCallerSet(address indexed transcoder, address indexed rewardCaller); + +// New state +mapping(address => address) public transcoderToRewardCaller; + +// New functions +function setRewardCaller(address _rewardCaller) external; +function rewardForTranscoder(address _transcoder) external; +function rewardForTranscoderWithHint(address _transcoder, address _newPosPrev, address _newPosNext) public; +``` + +`transcoderToRewardCaller` maps each orchestrator to the single address it has authorized to call `reward()` on its behalf. + +`setRewardCaller(address _rewardCaller)`: + +- Sets `transcoderToRewardCaller[msg.sender] = _rewardCaller`, authorizing `_rewardCaller` to call `reward()` for the caller. Passing `address(0)` unsets it. Emits `RewardCallerSet(msg.sender, _rewardCaller)`. +- Reverts if the system is paused. + +`rewardForTranscoderWithHint(address _transcoder, address _newPosPrev, address _newPosNext)`: + +- Behaves identically to `rewardWithHint()` (same optional `_newPosPrev`/`_newPosNext` pool-position hint), but mints for `_transcoder` rather than `msg.sender`. +- Reverts under the same conditions as `rewardWithHint()` (system paused, round not initialized, `_transcoder` not an active transcoder, or already called reward this round), plus if the caller is not the transcoder's configured reward caller. + +`rewardForTranscoder(address _transcoder)` is a convenience wrapper for `rewardForTranscoderWithHint(_transcoder, address(0), address(0))`. + +All reward accounting — the active-transcoder check, the once-per-round guard, the bonding checkpoint (and thus governance voting power), and the `Reward` / `TreasuryReward` events — is keyed on `_transcoder`, never on the caller. The existing `reward()` and `rewardWithHint()` retain their signatures and behavior. + +## Specification Rationale + +This LIP authorizes the reward caller with a mapping keyed by the orchestrator and a dedicated `rewardForTranscoder` entrypoint. This keeps the change small and simple to reason about, is griefing-resistant by construction (an orchestrator can only set its own caller), and leaves the existing `reward()` behavior untouched. + +Other options were considered and set aside: + +- **Fully permissionless `reward()`** — the simplest option, but set aside in governance discussion because it removes any requirement for an orchestrator to actively participate in claiming rewards, weakening the link between rewards and real network contribution. +- **Reusing the existing `reward()` for the caller**, instead of adding a `rewardForTranscoder` entrypoint — this avoids new functions and a client change, but keeping it griefing-safe requires more protocol complexity and modifies the audited reward path, so the dedicated entrypoint was preferred. + +## Backwards Compatibility + +This change does not introduce any backwards incompatibilities. It is purely an addition to the existing `BondingManager` API rather than a modification: `reward()` and `rewardWithHint()` keep their signatures and behavior, and an orchestrator that never calls `setRewardCaller` is entirely unaffected. An orchestrator that has set a reward caller can still call `reward()` itself. + +To use delegation, the node is run with the reward-caller key while targeting its orchestrator address — a small, additive go-livepeer change that only takes effect when a reward caller is configured. Orchestrators that do not configure one are unaffected and require no client change. + +## Test Cases + +See `test/unit/BondingManager.js` in https://github.com/livepeer/protocol/pull/648/files. + +## Implementation + +See `contracts/bonding/BondingManager.sol` and `contracts/bonding/IBondingManager.sol` in https://github.com/livepeer/protocol/pull/648/files. + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). From 4d0a753e5a9e970bba8e4ec28b0051ac9f1cc0ea Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Fri, 3 Jul 2026 17:28:44 +0200 Subject: [PATCH 2/4] Update sidestream author name Co-authored-by: SidestreamCrunchyCarrot --- LIPs/LIP-delegated-reward-calling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LIPs/LIP-delegated-reward-calling.md b/LIPs/LIP-delegated-reward-calling.md index e3e26c4..fa03e62 100644 --- a/LIPs/LIP-delegated-reward-calling.md +++ b/LIPs/LIP-delegated-reward-calling.md @@ -1,7 +1,7 @@ --- lip: 108 title: Delegated Reward Calling -author: Rick Staa (@rickstaa), (@), Nico Vergauwen (@kyriediculous) +author: Rick Staa (@rickstaa), Crunchy Carrot (@SidestreamCrunchyCarrot), Nico Vergauwen (@kyriediculous) type: Standard Track status: Draft created: 2026-07-02 From 75267a91bd4d3098107d845ceb006861f34c78c3 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Fri, 3 Jul 2026 18:28:20 +0200 Subject: [PATCH 3/4] chore: add forum discussion thread --- LIPs/LIP-delegated-reward-calling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LIPs/LIP-delegated-reward-calling.md b/LIPs/LIP-delegated-reward-calling.md index fa03e62..a3db732 100644 --- a/LIPs/LIP-delegated-reward-calling.md +++ b/LIPs/LIP-delegated-reward-calling.md @@ -5,7 +5,7 @@ author: Rick Staa (@rickstaa), Crunchy Carrot (@SidestreamCrunchyCarrot), Nico V type: Standard Track status: Draft created: 2026-07-02 -discussions-to: +discussions-to: https://forum.livepeer.org/t/lip-delegated-reward-calling-discussion-thread/3278 --- ## Abstract From b4f923b27988fa7184475f8d88013d3cc3416ec2 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sat, 4 Jul 2026 20:39:57 +0200 Subject: [PATCH 4/4] docs: assign LIP 118 Assign 118 to this LIP according to the convention to allign it with the authoring PR nr. --- LIPs/{LIP-delegated-reward-calling.md => LIP-118.md} | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename LIPs/{LIP-delegated-reward-calling.md => LIP-118.md} (99%) diff --git a/LIPs/LIP-delegated-reward-calling.md b/LIPs/LIP-118.md similarity index 99% rename from LIPs/LIP-delegated-reward-calling.md rename to LIPs/LIP-118.md index a3db732..889f961 100644 --- a/LIPs/LIP-delegated-reward-calling.md +++ b/LIPs/LIP-118.md @@ -1,5 +1,5 @@ --- -lip: 108 +lip: 118 title: Delegated Reward Calling author: Rick Staa (@rickstaa), Crunchy Carrot (@SidestreamCrunchyCarrot), Nico Vergauwen (@kyriediculous) type: Standard Track diff --git a/README.md b/README.md index db2a7d1..b9b07c9 100644 --- a/README.md +++ b/README.md @@ -40,4 +40,4 @@ The first PR should contain the first draft of an LIP. An editor will manually r | [100](LIPs/LIP-100.md) | Introduction of Inflation Bounds in the Inflation Adjustment Algorithm | Abandoned | | [101](LIPs/LIP-101.md) | Restart Treasury Reward Cut | Final | | [107](LIPs/LIP-107.md) | Recalibrate LPT Emissions Parameters | Rejected | - +| [118](LIPs/LIP-118.md) | Delegated Reward Calling | Draft |