Minimal Issuance Reform: Linear Tapering
tl;dr
- Ethereum’s existing issuance curve incentivises growth in the staking ratio to levels which weaken security and make the protocol more open to capture;
- This is a minimal proposal to ensure stake growth beyond 50% is no longer incentivised, by applying a linearly-tapered offset to validator rewards;
- The underlying reward curve and the mechanism of rewards and penalties — including
BASE_REWARD_FACTOR— are left entirely unchanged; - Worst-case issuance is reduced to around 0.5%, and is likely to settle at a much lower value, derived from the market-determined reservation yield.
Introduction
As previously noted, Ethereum’s issuance curve incentivises growth in the staking ratio well beyond the point at which additional stake increases Ethereum’s security, and arguably makes it more brittle and vulnerable to capture. The level of issuance grows continuously as more Ether is staked, and even with all Ether staked, the nominal yield never goes below 1.5%. The current issuance curve therefore does not allow the market to reach an equilibrium yield, and we should expect the staking ratio to tend towards 100% over time.
To prevent Ethereum from reaching a problematic high staking ratio, this proposal introduces the simplest possible change to ensure that the staking market reaches an equilibrium yield at some point before 50% of all Ether has been staked. This is achieved without altering the existing reward curve and micro-incentives, by subtracting a per-epoch offset for each validator duty, with the offset tapered according to the staking ratio.
Yield and Issuance Curves
Considering our focus on the staking ratio, here we will express characteristic curves as functions of \(f\) (the fraction of total supply staked): the annual yield \(y(f)\) a validator earns on staked ETH,1 and the issuance \(i(f)\) — new ETH minted per year, as a fraction of total supply.
Under Ethereum’s current rules the yield follows
\[y(f) = \frac{B\,E}{\sqrt{f\,S}},\]
with BASE_REWARD_FACTOR \(B\), epochs per year \(E\), and total ETH supply \(S\) in Gwei. Issuance is the yield paid on the staked fraction,
\[i(f) = B\,E\,\sqrt{\frac{f}{S}}.\]
Linear tapering subtracts from the yield a term that is zero at \(f = 0\) and grows linearly to cancel the yield entirely at the saturation ratio \(f_\text{sat}\):
\[\tilde y(f) = \begin{cases} y(f) - \dfrac{f}{f_\text{sat}}\,y(f_\text{sat}) & f \le f_\text{sat} \\ 0 & f > f_\text{sat} \end{cases}\]
With BASE_REWARD_FACTOR left at its current value of \(B = 64\), the taper reduces the net yield across the whole range, with the reduction growing as the staking ratio rises and reaching the full yield at \(f_\text{sat}\). The tapered curve crosses zero at \(f_\text{sat} = 50\%\) and stays there beyond it, removing any incentive to stake past that point.
The tapered issuance follows directly,
\[\tilde i(f) = \begin{cases} f\,\tilde y(f) & f \le f_\text{sat} \\ 0 & f > f_\text{sat} \end{cases}\]
and no longer runs away: it rises, peaks at \(f^* = 2^{-7/3} \approx 19.8\%\), and falls back to zero at \(f_\text{sat}\).
The Offset Fraction
The offset deducts a fraction \(q(f)\) of each validator’s ideal reward, leaving net yield \((1-q(f))\,y(f)\). To reproduce the subtractive linear taper \(\tilde y(f)\) above, this fraction must be
\[q(f) = \left(\frac{f}{f_\text{sat}}\right)^{3/2}.\]
The power of \(3/2\) rather than \(1\) is a consequence of the square-root reward curve: the deduction itself is linear in \(f\), but the reward it is taken from scales as \(f^{-1/2}\), so expressed as a fraction of that reward the offset picks up an extra half power.
From Staking Fraction to Absolute Balance
Everything up to this point has been expressed in terms of the staking ratio \(f\), which keeps the rationale clear and matches the curves above. The protocol itself, however, cannot observe \(f\) directly: it knows the total active balance but not the total ETH supply. The spec below therefore works in absolute terms, using the total active balance \(D = f\,S\) in place of \(f\), and a fixed constant \(D_\text{sat} = f_\text{sat}\,S\) (SATURATION_BALANCE) in place of \(f_\text{sat}\). The offset fraction is unchanged in form,
\[q = \left(\frac{D}{D_\text{sat}}\right)^{3/2}.\]
Because \(D_\text{sat}\) is fixed at the hard fork rather than tracking the live supply, the effective saturation ratio will drift slowly as the supply changes through issuance and burning. The drift is small, and could be removed by a future fork that makes the protocol aware of the total supply — allowing \(D_\text{sat}\) to be recovered from \(f_\text{sat}\,S\) directly rather than baked in as a constant.
Per-duty implementation
A validator’s assigned duties are attestation (every epoch), and occasional block proposals and sync committee participation. The offset is correspondingly applied at the epoch boundary in three parts, each deducting \(q\) times an idealised validator reward for each duty:
- Attestation offset — a deduction of \(q\) times the ideal attestation reward from every active validator;
- Proposer offset — a deduction of \(q\) times an equal share of the epoch’s ideal proposer reward, charged to each of the 32 proposers;
- Sync offset — a deduction of \(q\) times the ideal sync committee reward for the epoch from each of the 512 sync committee members.
Targeting offsets at individual duties in this way, rather than making a uniform per-validator deduction, reduces the variance between validators due to the rare duties of proposal and sync committee participation.
Consensus Layer Spec
The changes below leave the rewards-and-penalties machinery entirely untouched — BASE_REWARD_FACTOR included — adding only the calculation and deduction of issuance offsets during epoch processing: a new SATURATION_BALANCE constant is added, a helper get_tapered_offset computes the offset for a given reward, and a new step process_issuance_offsets applies it. That step also relies on two lookups not in the current spec: get_beacon_proposer_index_at_slot2 and get_validator_index_by_pubkey.3
New Constant
| Constant | Value | Notes |
|---|---|---|
SATURATION_BALANCE |
Gwei(60_250_000 * 10**9) |
Total staked balance \(D_\text{sat}\) at \(f_\text{sat} = 50\%\); set at the hard fork to approximately half the current ETH supply |
SATURATION_BALANCE encodes \(D_\text{sat} = f_\text{sat} \cdot S\) in Gwei.
New Function: the offset fraction
The offset fraction is
\[q = \left(\frac{D}{D_\text{sat}}\right)^{3/2} = \left(\frac{\sqrt{D}}{\sqrt{D_\text{sat}}}\right)^{3}.\]
Writing it as the cube of the square-root ratio lets us apply it to a reward as three successive uint64 multiply-divides, so every intermediate stays well within uint64 — consistent with the rest of the reward machinery, which never forms a value wider than 64 bits. A single helper applies the cubed ratio to a reward, taking the two square roots (computed once per epoch by the caller) as arguments:
def get_tapered_offset(reward: Gwei, sqrt_active: uint64, sqrt_sat: uint64) -> Gwei:
# reward * (sqrt_active / sqrt_sat)**3 == reward * (D / SATURATION_BALANCE)**(3/2)
offset = uint64(reward)
for _ in range(3):
offset = offset * sqrt_active // sqrt_sat
return Gwei(offset)New Step: applying the offset
As described above, the offset splits by duty, so we apply it in three passes:
def process_issuance_offsets(state: BeaconState) -> None:
# Clamping sqrt_active to sqrt_sat to prevent negative issuance.
sqrt_sat = integer_squareroot(SATURATION_BALANCE)
sqrt_active = min(integer_squareroot(get_total_active_balance(state)), sqrt_sat)
increment = EFFECTIVE_BALANCE_INCREMENT
base = get_base_reward_per_increment(state)
epoch = get_current_epoch(state)
# 1. Attester offset — every active validator.
attest_weight = TIMELY_SOURCE_WEIGHT + TIMELY_TARGET_WEIGHT + TIMELY_HEAD_WEIGHT
for index in get_active_validator_indices(state, epoch):
n = state.validators[index].effective_balance // increment
attest_reward = Gwei(n * base * attest_weight // WEIGHT_DENOMINATOR)
offset = get_tapered_offset(attest_reward, sqrt_active, sqrt_sat)
decrease_balance(state, index, offset)
# 2. Proposer offset — the 32 proposers of the epoch.
n_total = get_total_active_balance(state) // increment
ideal_proposer_reward = Gwei(
n_total * base * PROPOSER_WEIGHT // WEIGHT_DENOMINATOR // SLOTS_PER_EPOCH
)
proposer_offset = get_tapered_offset(
ideal_proposer_reward, sqrt_active, sqrt_sat
)
start_slot = compute_start_slot_at_epoch(epoch)
for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH):
proposer = get_beacon_proposer_index_at_slot(state, Slot(slot))
decrease_balance(state, proposer, proposer_offset)
# 3. Sync offset — the 512 sync committee members.
for pubkey in state.current_sync_committee.pubkeys:
index = get_validator_index_by_pubkey(state, pubkey)
n = state.validators[index].effective_balance // increment
sync_reward = Gwei(n * base * SYNC_REWARD_WEIGHT // WEIGHT_DENOMINATOR)
offset = get_tapered_offset(sync_reward, sqrt_active, sqrt_sat)
decrease_balance(state, index, offset)process_issuance_offsets is added to process_epoch immediately after process_rewards_and_penalties and before process_effective_balance_updates. Running it there means effective balances still hold the epoch’s values, so step 2 charges exactly the proposers that were selected during the epoch.
The dominant new cost is one O(1)-per-validator deduction (the attestation offset) — foldable into the existing rewards/penalties pass — plus two fixed passes over the 32 proposers and 512 sync committee members.
Footnotes
Throughout, \(y(f)\) refers to consensus-layer issuance yield only; it does not include execution-layer yield (priority fees and MEV). Execution-layer yield is in any case unpredictable, and there is an expectation that it may eventually be burned rather than passed to the validator.↩︎
The existing
get_beacon_proposer_indexgeneralised to an arbitrary slot in the epoch.↩︎A pubkey→index lookup, which clients already maintain as a cache.↩︎