Minimal Issuance Reform: Linear Tapering with a Transition Period
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; - To avoid a sudden drop in yield at the fork, the offset is phased in over a one-year transition: every deduction is scaled by a coefficient that ramps linearly from zero to one, so net yield slides smoothly from today’s curve to the fully tapered curve;
- 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.
This variant of the proposal adds one further refinement: rather than applying the full offset the moment the fork activates, it phases the offset in over a one-year transition period, so that validators experience a gradual change in yield rather than a step.
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.
This is the curve the protocol settles on once the transition is complete. During the transition the net yield lies between today’s curve and this one; the slider on the chart below sweeps through that one-year journey, from the current curve (month 0) to the fully tapered curve (month 12).
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.
Phasing in the Change
Applying the full fraction \(q(f)\) from the first epoch after the fork would step the net yield down discontinuously — at today’s ratio of roughly a third, the deduction is already a substantial fraction of the reward. To smooth this out we scale every offset by a transition coefficient \(c(t)\) that ramps linearly from zero to one over a transition period of length \(T\) beginning at the fork:
\[c(t) = \operatorname{clamp}\!\left(\frac{t - t_0}{T},\, 0,\, 1\right),\]
where \(t_0\) is the fork epoch and \(T \approx 1\text{ year}\). The effective offset fraction is then
\[q_\text{eff}(f, t) = c(t)\,q(f),\]
so the net yield during the transition is
\[\tilde y(f, t) = \bigl(1 - c(t)\,q(f)\bigr)\,y(f).\]
At \(t = t_0\) we have \(c = 0\) and \(\tilde y = y\) — exactly today’s curve, so the fork itself changes nothing. As \(c\) rises to one over the year, the curve slides down to the fully tapered \(\tilde y(f)\) of the previous section. Because the same coefficient multiplies every duty’s offset uniformly, the entire family of intermediate curves is just a scaled blend of the two endpoints, which is precisely what the chart sliders interpolate.
A linear ramp is the simplest choice and is what the spec below implements; a smoother (e.g. cosine) ramp would remove the slight kinks in yield at the start and end of the transition, at the cost of a less transparent specification.
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.
The transition coefficient \(c(t)\) is likewise expressed in epochs rather than abstract time: with \(t_0\) a fixed TRANSITION_START_EPOCH and \(T\) a fixed TRANSITION_DURATION_EPOCHS, the coefficient at epoch \(e\) is
\[c(e) = \operatorname{clamp}\!\left(\frac{e - t_0}{T},\, 0,\, 1\right).\]
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 \(c(e)\,q\) times an idealised validator reward for each duty:
- Attestation offset — a deduction of \(c(e)\,q\) times the ideal attestation reward from every active validator;
- Proposer offset — a deduction of \(c(e)\,q\) times an equal share of the epoch’s ideal proposer reward, charged to each of the 32 proposers;
- Sync offset — a deduction of \(c(e)\,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: three new constants (SATURATION_BALANCE plus the two transition parameters) are added, a helper get_tapered_offset computes the offset for a given reward, a helper get_transition_ramp computes the transition coefficient, 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 Constants
| 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 |
TRANSITION_START_EPOCH |
Epoch(...) |
Epoch \(t_0\) at which tapering begins; set at the hard fork to the fork’s activation epoch |
TRANSITION_DURATION_EPOCHS |
Epoch(82_125) |
Transition length \(T\); \(82{,}125 = 365 \times 225\) epochs \(\approx 1\) year |
SATURATION_BALANCE encodes \(D_\text{sat} = f_\text{sat} \cdot S\) in Gwei. There are \(225\) epochs per day, so TRANSITION_DURATION_EPOCHS spans 365 days.
New Function: the offset fraction
The offset fraction, scaled by the transition coefficient, is
\[q_\text{eff} = c(e)\left(\frac{D}{D_\text{sat}}\right)^{3/2} = c(e)\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, with the transition coefficient applied as a fourth, so every intermediate stays well within uint64 — consistent with the rest of the reward machinery, which never forms a value wider than 64 bits. The transition coefficient enters as an integer ramp numerator over the fixed duration denominator (both supplied by the caller, computed once per epoch), and is applied last so it scales the already-reduced offset:
def get_tapered_offset(
reward: Gwei,
sqrt_active: uint64,
sqrt_sat: uint64,
ramp: uint64,
ramp_denominator: uint64,
) -> Gwei:
# reward * (sqrt_active / sqrt_sat)**3 * (ramp / ramp_denominator)
# == reward * (D / SATURATION_BALANCE)**(3/2) * c(epoch)
offset = uint64(reward)
for _ in range(3):
offset = offset * sqrt_active // sqrt_sat
return Gwei(offset * ramp // ramp_denominator)New Function: the transition ramp
The ramp numerator climbs by one each epoch from the start of the transition, capped at TRANSITION_DURATION_EPOCHS; dividing by that same duration yields the coefficient \(c(e)\) ramping from \(0\) to \(1\). Before the transition begins the numerator is zero, so no offset is applied:
def get_transition_ramp(epoch: Epoch) -> uint64:
# Returns the numerator of c(epoch) = ramp / TRANSITION_DURATION_EPOCHS,
# which ramps linearly from 0 (at TRANSITION_START_EPOCH) to 1
# (after TRANSITION_DURATION_EPOCHS epochs have elapsed).
if epoch <= TRANSITION_START_EPOCH:
return uint64(0)
elapsed = epoch - TRANSITION_START_EPOCH
return min(uint64(elapsed), TRANSITION_DURATION_EPOCHS)New Step: applying the offset
As described above, the offset splits by duty, so we apply it in three passes. The square roots and the transition ramp are computed once for the epoch and passed to every offset calculation:
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)
# Transition coefficient c(epoch) = ramp / TRANSITION_DURATION_EPOCHS.
ramp = get_transition_ramp(epoch)
ramp_denominator = uint64(TRANSITION_DURATION_EPOCHS)
# 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, ramp, ramp_denominator)
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, ramp, ramp_denominator
)
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, ramp, ramp_denominator)
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.
Once epoch - TRANSITION_START_EPOCH reaches TRANSITION_DURATION_EPOCHS, the ramp saturates at its denominator, the coefficient is permanently one, and the behaviour is identical to the non-transitioned proposal — the transition logic adds only a single min and one extra multiply-divide per offset, and falls away entirely thereafter.
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.↩︎