livepeer / LIPs

Livepeer Improvement Proposals
9 stars 13 forks source link

LIP-56: Reward Period #56

Closed yondonfu closed 1 year ago

yondonfu commented 3 years ago

This is the discussion thread for LIP-56: Reward Period which proposes introducing a reward to reduce the frequency of reward calls and the overall costs incurred by orchestrators that are responsible for calling reward. The draft was recently merged. All feedback is welcome! All reviewers should refer to the LIP text for the latest version of the proposal.

dob commented 3 years ago

Let's say an O does not call reward during an eligible rewardRound. What would happen to the LPT that had been previously minted during the initializeRound txn, that would have been allocated to this O? It just sits in the minter lost forever?

yondonfu commented 3 years ago

Let's say an O does not call reward during an eligible rewardRound. What would happen to the LPT that had been previously minted during the initializeRound txn, that would have been allocated to this O? It just sits in the minter lost forever?

Yes.

The current approach in the LIP is the easiest way to accumulate rewards during the reward period based on the per round inflation rate that I can think of at the moment. But, I also realized that the current approach results in the total LPT supply increasing each round without also increasing the total active staked LPT. So, if no additional LPT is staked by delegators, then the participation rate would be decreasing in each round of a reward period.

Will have to think a bit more about whether there is a reasonable alternative to avoid these outcomes.

dob commented 3 years ago

Without opening a can of worms into "what any unclaimed LPT should be used for", it might make sense simply to add a mechanism to this LIP to appropriately account for permanently unclaimed LPT. Then in a future LIP, the community could access that unclaimed LPT for a specific purpose (like dev fund, or burn, etc).

Maybe the way to account for it is to enable a transaction that simply updates an accounting variable with the unclaimed LPT amount for a previous round. Or moves the LPT into another upgradeable contract and debits it from the Minter.

kyriediculous commented 3 years ago

After looking through the spec and the draft implementation I'm not entirely sure still how stake updates are handled within a single rewardPeriod.

An orchestrator's stake could still increase within a given rewardPeriod and currently we do not account that rewards before that update should not be calculated according to the new updated stake because we only do the calculation at the end.

For example, let's assume 1000 LPT is minted each round

  1. current reward period is round N up until round (and not including) N + 7

  2. Up until (and including) round N + 2, 3000 LPT is minted and the orchestrator's network ownership is 10% For these rounds the orchestrator is owed3000 LPT * 0.10 = 300 LPT in rewards

  3. In round N+2 the orchestrator receives a stake update, which will become effective in round N+3, that updates its network ownership to 20%.

This sets the orchestrator's lastActiveStakeUpdateRound = N+3 and transcoder.EarningsPoolForRound[N+3].totalStake = 20% of all stake

  1. Another 4000 LPT is minted for these subsequent rounds N+3 until (and not including) N + 7 The orchestrator is owed 4000 LPT * 0.20 = 800 LPT for this round

=> The total rewards for the orchestrator for the rewardPeriod is 1100 LPT

But the current state of the implementation only considers the end stake after the update, so it will use a network ownership factor of 20% even for the rounds it was only 10% resulting in a discrepancy of the rewards calculated.

// rewardTokens = (minted tokens for the reward period * active transcoder stake) / total active stake
rewardTokens = MathUtils.percOf(roundsManager().mintedInRewardPeriod(), earningsPool.totalStake, currentRoundTotalActiveStake);

Or 7000 LPT * 0.20 = 1400 LPT


Or am I missing something here?

kyriediculous commented 3 years ago

Perhaps it could be a good idea to describe the potential user outcomes in the test scenario's in plain english, some examples:

kyriediculous commented 3 years ago

So, if no additional LPT is staked by delegators, then the participation rate would be decreasing in each round of a reward period.

One solution I think is to assume that all LPT minted during the rewardPeriod is stake that will participate (IIUC this will keep the participation rate as is) , then we can keep track how much LPT is unclaimed for during the actual reward Round and upon the next round initialisation do the actual accounting ?

What exactly should happen to the "leftover" LPT other than burning the leftover upon the next round init is , in my opinion, out of scope for this LIP. Burning would most closely resemble the current contract mechanics.

dob commented 3 years ago

burning the leftover upon the next round init

Yah, I agree that the scope of what could happen is huge and probably shouldn't be discussed within this LIP. Good idea to handle the accounting (or burning) in the next initRound txn, which would mean that there isn't a growing pool of unaccounted for LPT.

kyriediculous commented 3 years ago

A bit late, but I mentioned during the last LIP call that I would share my design for LIP-56 publicly so here it is.

This design also removes the inflation logic out of the Minter into a separate InflationManager contract, perhaps for clarity it might be better to move this into a separate LIP as a package deal.

Edit: Looks like github markdown doesn't parse some of the equations correctly, so here's the hackmd file for reference: https://hackmd.io/GxpinLeJSeOo6xX3bCJJkQ


LIP-56 Reward Period

Abstract

This proposal introduces a reward period which can reduce the frequency of reward calls (reward distribution transactions) and the overall costs incurred by orchestrators that are responsible for calling reward.

Motivation

Gas prices on Ethereum have been fairly high due to increased demand for blockspace. In recent months the transaction cost of calling reward daily in order to receive inflationary LPT rewards often exceeds or is only marginally lower than the value received for many Orchestrators on the network.

To reduce the cost burden we could change the reward algorithm to require less frequent reward calls in tradeoff for a slightly higher single transaction cost.

E.g. Let's say a reward call under current conditions (per round basis) costs 100$. Introducing a reward period would increase the cost to 150$ for the call, but the frequency of calls reduces by 7x.

This still results in a cost saving of several magnitudes, in this example a 4.6x reduction:

100$ * 7 / 150$ = 4.6

Specification

Rewards are minted for each rewardPeriod rather than each round. Transcoders can call reward to claim their earnings for a rewardPeriod N during the entirety of rewardPeriod N + 1. The new frequency at which reward needs to be called will be equal to rewardPeriodlength. If a transcoder joins or leaves the active transcoder set during a rewardPeriod it will still be eliglbe to claim rewards for the rounds it was actually active for during the rewardPeriod.

InflationManager (new)

The InflationManager is a new contract responsible for inflation management, logic which is now part of the Minter contract.

This separation of concerns allows for a more flexible upgrade path for inflation related parameters and logic in the future. Currently the Minter acts as both a "Bank" for user funds and inflation manager. Any upgrade to the Minter contract requires a migration of these user funds, thus ideally the Minter contract should change as infrequent as possible.

State

Parameter Type Description
inflation uint256 Per round inflation rate
targetBondingRate uint256 Target bonding rate as a percentage of total bonded tokens / total token supply
inflationChange uint256 Change in inflation rate per round until the target bonding rate is achieved
rewardTokensForRound mapping(uint256=>uint256) Mapping holding the current total mintable reward tokens for a round
mintedInRewardPeriod uint256 Track how many tokens are already minted in the current eligible reward period
tokensForRewardPeriod uint256 Track how many tokens are available for the eligible reward period (sum of rewardTokensForRound for a reward period)

API

inflation (migrated from Minter)
function inflation() external view returns (uint256);

Get the current inflation rate. This is a percentage scaled by factor 1 000 000 000.

targetBondingRate (migrated from Minter)
function targetBondingRate() external view returns (uint256);

Get the current target bonding rate. This is a percentage scaled by factor 1 000 000 000.

i.e. 50% = 0.5 * 1 000 000 000 = 500 000 000

inflationChange (migrated from Minter)
function inflationChange() external view returns (uint256);

Get the current inflation change. This is a percentage scaled by factor 1 000 000 000.

setTargetBondingRate (migrated from Minter)
 function setTargetBondingRate(uint256 _targetBondingRate) external onlyControllerOwner;

Set the current target bonding rate. This is a percentage scaled by factor 1 000 000 000.

Only callable by the owner of Controller.

setInflationChange (migrated from Minter)
function setInflationChange(uint256 _inflationChange) external onlyControllerOwner;

Set the current inflation change. This is a percentage scaled by factor 1 000 000 000.

Only callable by the owner of Controller.

setRewardTokensForRound (new)
function setRewardTokensForRound(uint256 _round, uint245 _amount) external onlyRoundsManager

Sets the amount of reward tokens that are mintable for a particular round in the rewardTokensForRound mapping, this will likely always be currentRound when this function called upon round initialization.

Only callable by the RoundsManager.

The amount of tokens that is mintable for _round is calculated as follows:

$r = {totalSupply}\times inflation$

uint256 rewardTokens = MathUtils.percOf(
    livepeerToken().totalSupply(),
    inflation
);
rewardTokensForRound (new)
function rewardTokensForRound(uint256 _round) external view returns(uint256);

Gets the amount of reward tokens that are mintable for a given round.

mintedInRewardPeriod (new)
function mintedInRewardPeriod() public view returns (uint256);

Returns the current amount total minted tokens for the current eligiblerewardPeriod (defined as the last ended rewardPeriod).

Called by BondingManager to see if the amount of total rewards for the rewardPeriod so far doesn't exceed the minimum set forth during round initialization.

uint256 currentMintedtokens = currentMintedTokens.add(rewardAmount);

require(
    currentMintedTokens <= rewardTokensForPeriod,
    "minted tokens cannot exceed mintable tokens"
);

mintedInRewardPeriod and tokensForRewardPeriod will be reset when a new reward period starts.

setMintedInRewardPeriod (new)
setMintedInRewardPeriod(uint256 _amount)

Sets the tokens minted so far in the current eligible reward period. Resets when a new reward period starts.

Can only be called by the BondingManager and RoundsManager.

If _amount is 0, reset mintedInRewardPeriod and tokensForRewardPeriod, as this indicates a new reward period starts.

Minter

The minter will have a simplified API and is no longer concerned with inflation calculations. It will solely be responsible for minting, holding and sending ETH and LPT.

API

migrateToNewMinter (unchanged)

Migrates the current Minter to a new deployment. Internally transfers all balances to the new contract.

Only callable by the owner of Controller

 function migrateToNewMinter(IMinter _newMinter) external onlyControllerOwner whenSystemPaused;
trustedTransferTokens (unchanged)
function trustedTransferTokens(address _to, uint256 _amount) external onlyBondingManager whenSystemNotPaused;

Transfers _amount Livepeer Token from the Minter to the recipient _to.

Only callable by the BondingManager when the system is not paused.

trustedBurnTokens (unchanged)
function trustedTransferTokens(address _to, uint256 _amount) external onlyBondingManager whenSystemNotPaused;

Burns _amount Livepeer Token.

Only callable by the BondingManager when the system is not paused.

trustedWithdrawEth (unchanged)
function trustedWithdrawETH(address payable _to, uint256 _amount) external onlyBondingManagerOrJobsManager whenSystemNotPaused;

Transfers _amount ETH from the Minter to the recipient _to.

Only callable by the BondingManager or TicketBroker when the system is not paused.

depositETH (unchanged)
function depositETH() external payable onlyMinterOrJobsManager returns (bool)
createReward (changed)
function createReward(uint256 _rewardTokens) external onlyBondingManager whenSystemNotPaused

Mints _rewardTokens where _rewardTokens is the total amount of mintable tokens for a transcoder for a given rewardPeriod, a rewardPeriod can consist of multiple rounds.

Calculating the number of _rewardTokens is now a responsability of the BondingManager instead.

BondingManager

State

Following state variables are added/changed for the BondingManager

Parameter Type Description
currentRoundTotalActiveStake uint256 DEPRECATED
nextRoundTotalActiveStake uint256 DEPRECATED
activeStakeForRound mapping(uint256=>uint256) Mapping keeping track of the total amount of active stake for a round

API

currentRoundTotalActiveStake (deprecated)

Deprecated in favor of activeStakeForRound() and setActiveStakeForRound()

nextRoundTotalActiveStake (deprecated)

Deprecated in favor of activeStakeForRound() and setActiveStakeForRound()

activeStakeForRound (new)

Retrieves the active stake for a round. This can be currentRound or a round in the past.

replaces BondingManager.currentRoundActiveStake and BondingManager.nextRoundTotalAtiveStake

function activeStakeForRound(uint256 _round) public returns (uint256)
setActiveStakeForRound (new)

Sets or updates the active stake for a round. In as good as every case this will be for currentRound + 1.

If activeStakeForRound(currentRound + 1) == 0, use the activeStakeForRound(currentRound) as a starting value to add to or substract from.

function setActiveStakeForRound(uint256 _round, uint256 _amount) external onlyRoundsManager
reward (implementation change)
  1. Check that caller didn't already claim rewards for the eligible rewardPeriod

Reward period eligble for reward:

{
roundsManager().previousRewardPeriodStart()
..
roundsManager().currentRewardPeriodStart() - 1
}
require(
    transcoder.lastRewardRound < roundsmanager().previousRewardPeriodStart(),
    "Already called reward in current eligible reward period"
);
  1. Check if caller is/was an active transcoder during the eligible rewardPeriod, revert if this is not the case.
    uint256 previousRewardPeriodStart = roundsmanager().previousRewardPeriodStart()
    require(
        transcoder.deactivationRound >= previousRewardPeriodStart && 
        transcoder.activationRound < previousRewardPeriodStart.add(roundsManager().rewardPeriodLength()),
        "transcoder inactive during reward period"
    );`
  1. Get the start and end rounds for the caller; this will be the range defined in step (1), unless the trancoder activated/deactivated within the current eligible rewardPeriod

    uint256 startRound = roundsManager().previousRewardPeriodStart();
    if (transcoder.activationRound >= startRound) {
        startRound = transcoder.activationRound;
    }
    
    uint256 endRound = roundsManager().currentRewardPeriodStart() - 1;
    if (transcoder.deactivationRound <= endRound) {
        endRound = transcoder.deactivationRound - 1;
    }
  2. Calculate the amount of rewards to be minted for {startRound..endRound} as defined in step (3)

$rewardTokens=\sum_{i=start}^{end} ri = r{start} + r{start+1}... + r{end-1} + r_{end}$

$r_i = {rewardTokensForRound_i}\times \frac{transcoderStakeForRound_i}{activeStakeForRound_i}$

  1. Mint rewardTokens and update the minted tokens for the current eligible reward period
uint256 currentMintedtokens = inflationManager().mintedInRewardPeriod() + rewardAmount;

require(
    currentMintedTokens <= inflationManager().tokensForRewardPeriod(),
    "minted tokens cannot exceed mintable tokens"
);

minter().createReward(rewardTokens);

inflationManager().setMintedInRewardPeriod(currentMintedTokens);
  1. call updateTranscoderWithRewards
updateTranscoderWithRewards(
    msg.sender,
    rewardTokens,
    currentRound,
    _newPosPrev,
    _newPosNext
);

This call will update the transcoder's lastActiveStakeUpdateRound to the start round of the next rewardPeriod.

RoundsManager

State

Following state variables are added to the RoundsManager

Parameter Type Description
rewardPeriodLength uint256 The number of rounds in a reward period, the initial value is 7
previousRewardPeriodStart uint256 The start round of the previous reward period
currentRewardPeriodStart uint256 The start round of the current reward period

API

setRewardPeriodlength (new)
function setRewardPeriodlength(uint256 _rewardPeriodLength) external onlyControllerOwner

Set the number of rounds in a reward period. If setRewardPeriodLength is called in the middle of a reward period, the length of the current reward period will be extended or shortened.

_rewardPeriodLength has to be greater or equal than 1 round.

Only callable by the owner of Controller.

initializeRound (implementation change)

The function signature will remain the same but some of the logic external contract calls that are part of this function will change

    function initializeRound() external whenSystemNotPaused {
        uint256 currRound = currentRound();

        // if the current reward period ends, update the state
        if (currRound >= currentRewardPeriodStart + rewardPeriodLength) {
            previousRewardPeriodStart = currentRewardPeriodStart;
            currentRewardPeriodStart = currRound

            // reset the amount of minted tokens for the eliglbe reward period
            // as we now enter a new eliglble reward period
            inflationManager.setMintedInRewardPeriod(0);
        }

        ... 

        // Set total active stake for the round
        bondingManager().setActiveStakeForRound(
            currRound,
            bondingManager().activeStakeForRound(currRound - 1);
        );
        // Set mintable rewards for the round
        inflationManager().setRewardTokensForRound(currRound)

        ... 
    }

Go-Livepeer

In go-livepeer the rewardService needs to be updated to only call reward at then end of a current rewardPeriod.

Specification Rationale

chrishobcroft commented 3 years ago

@kyriediculous thank you for putting this together - with gas prices seemingly growing again, and LPT market value falling, this is becoming more important.

For me, it would be good to see at least something written up for the "Motivation" for this design, to be able to understand whether we are aligned on this.

What are the next steps for this?

kyriediculous commented 3 years ago

Added a motivation and made some corrections to the spec.

Also still have to come up with a proper algorithm for step 4 in the new reward() logic.

yondonfu commented 3 years ago

This LIP has just been assigned the Last Call status which kicks of a 10 day Last Call period for any remaining feedback prior to the LIP being assigned the Proposed status.

yondonfu commented 3 years ago

Summary of a recent discussion with @kyriediculous:

It will be difficult to do the accounting required to distribute rewards to delegators on a round by round basis without also increasing gas costs substantially and the easier thing to do might be to only distribute rewards to delegators that are delegated to an orchestrator for the entirety of a reward period. This results in a less dynamic delegation market because it would mean delegators would change delegations only between reward periods instead of between rounds if they want to earn rewards. This is not desirable in the long term, but could be worth considering as a sacrifice in exchange for lower reward call costs in the short term given that delegation activity isn't expected to be very dynamic until delegation can be implemented on a L2/more scalable layer (which will be a big focus - it will just take longer to achieve which is why this proposal is being considered for the short term). Given the above:

The hope is to get the updated design out soon (here, in Discord, etc.) for feedback during the Last Call period.

yondonfu commented 3 years ago

Reward calls from the past 90 days

newplot Source: DuneAnalytics

The above chart plots the # of reward calls in each round and the average USD tx cost for a reward call in each round.

Observations:

# of active orchestrators in the past 90 days

numActive vs  round Source: GSheets

The above chart plots the # of active orchestrators in each round.

Observations:

Staking actions from the past 90 days

newplot (1) Source: DuneAnalytics

The above chart plots the # of staking actions [1] and the average USD tx cost for a staking action in each round.

Observations:

[1] Only transactions that emit Bond events are considered. Rebonding and withdrawals were not considered.

Thoughts:

kyriediculous commented 3 years ago

Third iteration of the design : https://hackmd.io/GxpinLeJSeOo6xX3bCJJkQ?view

LIP-55 InflationManager

Abstract

This proposal aims to seperate the current Minter contract into two separate contracts.

Motivation

Currently whenever inflation related logic has to change the entire Minter contract needs to be replaced, this involves migrating user funds to the new contract making it a very sensitive operation.

Seperation of concerns between funds management and inflation management allows for upgrades to the inflation logic and parameters without having to migrate user funds to a new contract.

The Minter contract itself would have to change very infrequently, on top of already being immutable, resulting in increased safety of user funds during protocol upgrades.

Specification

InflationManager

The InflationManager is a new contract responsible for inflation management, logic which is now part of the Minter contract.

State

Parameter Type Description
inflation uint256 Per round inflation rate
inflationChange uint256 Change in inflation rate per round until the target bonding rate is achieved
targetBondingRate uint256 Target bonding rate as a percentage of total bonded tokens / total token supply
currentMintableTokens uint256 Total mintable reward tokens for the current reward period
currentMintedTokens uint256 Track how many tokens are already minted in the current round
nextMintableTokens uint256 Total mintable reward tokens for the next reward period

API

inflation
function inflation() external view returns (uint256);

Get the current inflation rate. This is a percentage scaled by factor 1 000 000 000.

inflationChange
function inflationChange() external view returns (uint256);

Get the current inflation change. This is a percentage scaled by factor 1 000 000 000.

setInflationChange
function setInflationChange(uint256 _inflationChange) external onlyControllerOwner;

Set the current inflation change. This is a percentage scaled by factor 1 000 000 000.

Only callable by the owner of Controller.

targetBondingRate
function targetBondingRate() external view returns (uint256);

Get the current target bonding rate. This is a percentage scaled by factor 1 000 000 000.

i.e. 50% = 0.5 * 1 000 000 000 = 500 000 000

setTargetBondingRate
 function setTargetBondingRate(uint256 _targetBondingRate) external onlyControllerOwner;

Set the current target bonding rate. This is a percentage scaled by factor 1 000 000 000.

Only callable by the owner of Controller.

currentMintableTokens

function currentMintableTokens() uint256 public

Returns the amount of total mintable tokens for the current active reward period.

setCurrentMintableTokens
function setRewardTokensForRound(uint256 _round, uint245 _amount) external onlyRoundsManager

Sets the amount of reward tokens that are mintable for the current reward period.

Only callable by the RoundsManager.

The amount of tokens that is mintable for a round is calculated as follows:

$r = {totalSupply}\times inflation$

uint256 rewardTokens = MathUtils.percOf(
    livepeerToken().totalSupply(),
    inflation
);
currentMintedTokens
function currentMintedTokens() public view returns (uint256);

Returns the current total amount of tokens already minted in the current round.

Called by BondingManager to see if the amount of total rewards for the round doesn't exceed the minimum set forth during round initialization.

uint256 currentMintedtokens = currentMintedTokens.add(rewardAmount);

require(
    currentMintedTokens <= rewardTokensForPeriod,
    "minted tokens cannot exceed mintable tokens"
);

currentMintableTokens and currentMintedTokens will be reset when a new round starts.

setCurrentMintedTokens
setCurrentMintedTokens(uint256 _amount) internal 

Sets the tokens minted so far in the current eligible reward period. Resets when a new reward period starts.

Can only be called by the BondingManager and RoundsManager.

If _amount is 0, reset mintedInRewardPeriod and tokensForRewardPeriod, as this indicates a new reward period starts.

nextMintableTokens

function nextMintableTokens() uint256 public

Returns the amount of totable mintable tokens for the next reward period. This value is updated each round with the amount of inflationary LPT until all rounds for the reward period are accounted for.

setNextMintableTokens
function setRewardTokensForRound(uint256 _round, uint245 _amount) external onlyRoundsManager

Sets the amount of reward tokens that are mintable for the next reward period.

Only callable by the RoundsManager.

The amount of tokens that is mintable for a round is calculated as follows:

$r = {totalSupply}\times inflation$

uint256 rewardTokens = MathUtils.percOf(
    livepeerToken().totalSupply(),
    inflation
);

Minter

The Minter will hold inflationairy LPT that has been minted by reward calls and will be the only entity that can mint Livepeer Tokens.

The Minter will also hold any ETH from winning ticket redemptions until withdrawal.

API

migrateToNewMinter

Migrates the current Minter to a new deployment. Internally transfers all balances to the new contract.

Only callable by the owner of Controller

 function migrateToNewMinter(IMinter _newMinter) external onlyControllerOwner whenSystemPaused;
trustedTransferTokens
function trustedTransferTokens(address _to, uint256 _amount) external onlyBondingManager whenSystemNotPaused;

Transfers _amount Livepeer Token from the Minter to the recipient _to.

Only callable by the BondingManager when the system is not paused.

trustedBurnTokens
function trustedTransferTokens(address _to, uint256 _amount) external onlyBondingManager whenSystemNotPaused;

Burns _amount Livepeer Token.

Only callable by the BondingManager when the system is not paused.

trustedWithdrawEth
function trustedWithdrawETH(address payable _to, uint256 _amount) external onlyBondingManagerOrJobsManager whenSystemNotPaused;

Transfers _amount ETH from the Minter to the recipient _to.

Only callable by the BondingManager or TicketBroker when the system is not paused.

depositETH
function depositETH() external payable onlyMinterOrJobsManager returns (bool)
createReward
function createReward(uint256 _rewardTokens) external onlyBondingManager whenSystemNotPaused

Mints _rewardTokens where _rewardTokens is the total amount of mintable tokens for a transcoder for a given rewardPeriod, a rewardPeriod can consist of multiple rounds.

Calculating the number of _rewardTokens is now a responsability of the BondingManager instead.

LIP-56 Reward Period

Abstract

This proposal introduces a reward period which can reduce the frequency of reward calls (reward distribution transactions) and the overall costs incurred by orchestrators that are responsible for calling reward.

Motivation

Gas prices on Ethereum have been fairly high due to increased demand for blockspace. In recent months the transaction cost of calling reward daily in order to receive inflationary LPT rewards often exceeds or is only marginally lower than the value received for many Orchestrators on the network.

To reduce the cost burden we could change the reward algorithm to require less frequent reward calls in tradeoff for a slightly higher single transaction cost.

E.g. Let's say a reward call under current conditions (per round basis) costs $100. Introducing a reward period would increase the cost to $150 for the call, but the frequency of calls reduces by 7x.

This still results in a cost saving of several magnitudes, in this example a 4.6x reduction:

$100 * 7 / $150 = 4.6

Specification

Rewards are minted for each rewardPeriod rather than each round. Transcoders can call reward to claim their earnings for a rewardPeriod N during the entirety of rewardPeriod N + 1.

The new frequency at which reward needs to be called will be equal to rewardPeriodlength. A delegator and its delegates needs to be staked for the entirety of a rewardPeriod to be eligible for its rewards.

In more detail the rules are as follows:

In order to achieve this, a few core principles are introduced in this specification:

BondingManager

State

Following state variables are added to the global state.

Parameter Type Description
currentRewardPeriodTotalActiveStake private uint256 Total bonded stake for the current reward period
nextRewardPeriodTotalActiveStake private uint256 Total bonded stake for the next reward period

NOTE: Consider making variables private if they are not required for any 3rd party API

Following state is added to the Delegator type Parameter Type Description
nextClaimRewardsStart uint256 Start round for calculating cumulative rewards for the next claim earnings call
nextClaimRewardsPenalty uint256 Amount of stake to discount during the next claim earnings call if reward has been called for the start earningsPool for the delegate
Following state is added to the EarningsPool type Parameter Type Description
penaltyFromUnstake uint256 Penalty for this reward period from unstakes that occured during the period, used to calculate amount of tokens to be minted
rewardCalled boolean Indicates whether reward was called for the reward period, used to check whether the stake penalty for a delegate staked to the transcoder should be applied in case it did a staking action during the reward period

API

currentRewardPeriodTotalActiveStake
function currentRewardPeriodTotalActiveStake() public returns (uint256)

Returns the total active stake to be used in the rewards calculation for the current eligible reward period. Will be set to nextRewardPeriodTotalActiveStake by the RoundsManager when a new reward period starts.

nextRewardPeriodTotalActiveStake
function nextRewardPeriodTotalActiveStake() public returns (uint256)

Returns the total active stake to be used in the rewards calculation for the next reward period.

setCurrentRewardPeriodTotalActiveStake
function setCurrentRewardPeriodTotalActiveStake() external onlyRoundsManager

Sets the currentRewardPeriodTotalActiveStake equal to nextRewardPeriodTotalActiveStake.

Only callable by the RoundsManager

bondWithHint (implementation change)
unbondWithHint (implementation change)
rebondFromUnbonded

If the nextClaimStartRound < currentRound for a delegate, remove the stake discounts from the mapping to undo those state changes as we can technically count this as the delegate being staked for the full duration of the reward period.

reward (implementation change)
  1. Check that caller didn't already claim rewards for the eligible rewardPeriod

    Reward period eligble for reward:

    {
    previousRewardPeriodStart
    ..
    previousRewardPeriodEnd
    }
    require(
        transcoder.lastRewardRound < roundsmanager().previousRewardPeriodEnd(),
        "Already called reward for the current reward period"
    );
  2. Check if caller is/was an active transcoder during the eligible rewardPeriod, revert if this is not the case. This will be implicitly implied by the totalStake on the earningsPool for RoundsManager.previousRewardPeriodEnd() is 0.

        require(
            transcoder.earningsPoolForRound[RoundsManager().previousRewardPeriodEnd()] > 0,
            "transcoder inactive during reward period"
        );`
  3. Calculate the amount of rewards to be minted for {startRound..endRound}

    $r = {currentMintableTokens}\times \frac{totalStake_{delegate}}{currentRewardPeriodTotalActiveStake} - totalPenaltyFromUnstake$

    where totalPenaltyFromUnstake is the total amount of rewards that need to be discounted from the reward calculation due to unstaking activity happening for the delegate.

  4. Mint rewardTokens and update the minted tokens for the current eligible reward period

    uint256 currentMintedtokens = inflationManager().mintedInRewardPeriod() + rewardAmount;
    
    require(
        currentMintedTokens <= inflationManager().tokensForRewardPeriod(),
        "minted tokens cannot exceed mintable tokens"
    );
    
    minter().createReward(rewardTokens);
    
    inflationManager().setMintedInRewardPeriod(currentMintedTokens);
  5. call updateTranscoderWithRewards

    updateTranscoderWithRewards(
        msg.sender,
        rewardTokens,
        currentRound,
        _newPosPrev,
        _newPosNext
    );
  6. Set rewardCalled on the earningsPool for the end round for the reward period to ensure any penalties for delegate's are live.

claimEarnings

RoundsManager

State

Following state variables are added to the RoundsManager

Parameter Type Description
rewardPeriodLength uint256 The number of rounds in a reward period, the initial value is 7
previousRewardPeriodEnd uint256 The end round of the previous reward period
currentRewardPeriodEnd uint256 The end round of the current reward period
nextRewardPeriodEnd uint256 The end round of the next reward period

API

setRewardPeriodlength (new)
function setRewardPeriodlength(uint256 _rewardPeriodLength) external onlyControllerOwner

Set the number of rounds in a reward period. The changes goes into effect starting from the reward period after the next one.

_rewardPeriodLength has to be greater or equal than 1 round.

Only callable by the owner of Controller.

initializeRound (implementation change)

The function signature will remain the same but some of the logic external contract calls that are part of this function will change

Go-Livepeer

In go-livepeer the rewardService needs to be updated to not call reward every round, but every reward period instead.

Specification Rationale

yondonfu commented 3 years ago

Focusing on the LIP-56 design below and putting aside the InflationManager design for the moment.

I'll try to summarize the design in my own words at a high level:

I think the ability to call reward for a previous reward period anytime during the current reward period is a nice property. However, I'm a bit concerned about the complexity of the accounting implementation and edge cases that might arise.

I'm curious if the following may achieve most (would skip a few things though) of what we want in a simpler manner:

kyriediculous commented 3 years ago

I think your high level understanding of the design is on point and I think the way you framed some of the details could definitely help other people understand it as well.

I agree that the current way of accounting introduces some additional complexities, the main one being distinguishing "stake updates" from "reward eligible stake updates". However I think there are some beneficial tradeoffs to make here as well.

Or do you think I'm being too stubborn in terms of adversarial thinking here ?

Wisermerill commented 3 years ago

Hello all,

where is your thinking about the problematic of reward period ? cause for orchestrator like me, with good performances but very few LPT (and no one stacked from delegator at this point) it's the only hope we have to claim our inflation reward, and, may be, have some delegators stacking on our node.

Thanks,

Cianha

yondonfu commented 1 year ago

Closing because LIP-56 was assigned Abandoned due to inactivity.