Minimal Issuance Reform: Quadratic Offset Tapering
tl;dr
- The linear offset taper brings staking issuance to zero at a 50% staking ratio, but the net yield arrives there abruptly with a non-zero slope;
- Adding a single constraint — that the net yield reach zero tangentially — replaces the linear offset with a quadratic one, giving a smooth pinch-off;
- Nothing else in the linear proposal changes: the existing rewards and penalties stay untouched, the offset is still applied per duty, and the offset helper is reused verbatim — the quadratic taper is simply the linear offset times a per-epoch correction factor;
- The larger offset makes a higher
BASE_REWARD_FACTORnatural: \(B = 256\) crosses the current yield at \(f \approx 30.3\%\), close to today’s staking ratio.
Introduction
The linear offset taper subtracts from the staking yield a term that grows linearly in the staking fraction \(f\), cancelling the yield entirely at the saturation ratio \(f_\text{sat} = 50\%\). This version imposes one further condition: that the net yield reach zero with zero slope at \(f_\text{sat}\). This is most simply satisfied by a unique quadratic form for the offset. Everything else carries over from the linear proposal unchanged.
The Quadratic Offset
The reward curve is unchanged, with BASE_REWARD_FACTOR \(B\), epochs per year \(E\), and total ETH supply \(S\) in Gwei,
\[y(f) = \frac{B\,E}{\sqrt{f\,S}}, \qquad y'(f) = -\frac{y(f)}{2f}.\]
The linear taper subtracts \(\tfrac{f}{f_\text{sat}}\,y(f_\text{sat})\) — linear in \(f\), fixed only by the requirement that it vanish at \(f = 0\) and equal \(y(f_\text{sat})\) at saturation. Demanding in addition that the slope match at saturation (so the net slope there is zero) needs one more degree of freedom, i.e. a quadratic deduction. With \(f_\text{sat} = \tfrac12\) the unique quadratic through the origin meeting both endpoint conditions is \((5f - 6f^2)\,y(f_\text{sat})\), giving
\[\tilde y(f) = \begin{cases} y(f) - (5f - 6f^2)\,y(f_\text{sat}) & f \le f_\text{sat} \\ 0 & f > f_\text{sat} \end{cases}\]
This reaches zero at \(f_\text{sat}\) with \(\tilde y'(f_\text{sat}) = 0\). Once \(f_\text{sat}\) is fixed the only remaining freedom is BASE_REWARD_FACTOR, which scales \(y\) and \(y(f_\text{sat})\) together.
Applied naively with \(B = 64\), the tapered yield at today’s staking ratio is far below its current value. As in the linear case we can compensate for this by raising the BASE_REWARD_FACTOR. Given the larger quadratic offset, a greater increase is required to calibrate the yield at transition. \(B = 128\) crosses the current yield at \(f \approx 20.7\%\), while \(B = 256\) crosses at \(f \approx 30.3\%\) — close to today’s staking ratio — and is the more natural choice for the quadratic taper.
The tapered issuance follows directly, \(\tilde i(f) = f\,\tilde y(f)\) for \(f \le f_\text{sat}\) and zero beyond. It rises, peaks at \(f \approx 12.8\%\), and meets zero at \(f_\text{sat}\) with zero slope — the issuance curve is now tangent to the axis, where the linear taper hit it at an angle.
The Offset Fraction
As in the linear proposal the offset deducts a fraction \(q(f)\) of each validator’s ideal reward, leaving net yield \((1-q(f))\,y(f)\). With the quadratic deduction, and using \(y(f_\text{sat})/y(f) = \sqrt{f/f_\text{sat}} = \sqrt{2f}\) (since \(y \propto f^{-1/2}\)),
\[q(f) = (5f - 6f^2)\sqrt{2f} = \tfrac{1}{2}(2f)^{3/2}(5 - 6f),\]
replacing the linear fraction \(q(f) = (f/f_\text{sat})^{3/2}\). The factors of \(B\) cancel, so \(q\) is independent of BASE_REWARD_FACTOR. At saturation \(q(\tfrac12) = 1\) — the full reward is offset — and the tangency condition appears as \(q\) reaching 1 with exactly the slope that flattens the net-yield curve.
As in the linear proposal the spec cannot observe \(f\) directly, so it works in absolute terms using the total active balance \(D = f\,S\) and a fixed constant \(D_\text{sat} = f_\text{sat}\,S\) (SATURATION_BALANCE). Writing \(u = D/D_\text{sat}\) (so \(6f = 3u\)), the offset fraction is
\[q = \tfrac{1}{2}\,u^{3/2}\,(5 - 3u) = q_\text{lin}\cdot\tfrac{1}{2}(5 - 3u),\]
where \(q_\text{lin} = u^{3/2}\) is the linear proposal’s offset fraction. The quadratic taper is thus the linear one multiplied by a per-epoch correction factor \(\tfrac{1}{2}(5 - 3u)\) — which is exactly how the spec applies it below.
Per-duty implementation
The three-part split — an attestation offset on every active validator, a proposer offset on the epoch’s 32 proposers, and a sync offset on the 512 sync-committee members, each deducting \(q\) times the ideal reward for that duty — is exactly as in the linear proposal.
Consensus Layer Spec
The change to the consensus layer has the same shape as the linear proposal: BASE_REWARD_FACTOR is raised for calibration, SATURATION_BALANCE is added, a new get_tapered_offset helper computes the offset, and a new step process_issuance_offsets applies it during epoch processing. The rewards-and-penalties machinery is untouched, and the new step uses two lookups not in the current spec: get_beacon_proposer_index_at_slot1 and get_validator_index_by_pubkey.2
The only computational difference from the linear taper is the per-epoch correction factor \(\tfrac12(5-3u)\): since \(q = q_\text{lin}\cdot\tfrac12(5-3u)\), get_tapered_offset is identical to the linear proposal’s, and process_issuance_offsets scales each duty’s offset by that factor.
Updated Constant and New Constant
| Constant | Old value | New value | Notes |
|---|---|---|---|
BASE_REWARD_FACTOR |
uint64(64) |
uint64(256) |
4× (crossover \(f \approx 30.3\%\)); \(B = 128\) (crossover \(f \approx 20.7\%\)) is the alternative |
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 helper
Unchanged from the linear proposal. It applies the cubed square-root ratio \(u^{3/2} = (\sqrt{D}/\sqrt{D_\text{sat}})^3\) to a reward as three successive uint64 multiply-divides, so every intermediate stays well within uint64:
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
Identical to the linear step, except each duty’s offset — the linear \(q_\text{lin}\) value returned by get_tapered_offset — is scaled by the per-epoch correction factor \(\tfrac12(5-3u) = (5\,n_\text{sat} - 3\,n)/(2\,n_\text{sat})\), computed once. Clamping \(n\) to \(n_\text{sat}\) makes the factor 1 at and beyond saturation, where \(q = q_\text{lin} = 1\):
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)
# Quadratic correction (5 - 3 D/D_sat) / 2 — the only change from the linear taper.
n_sat = SATURATION_BALANCE // increment
n_active = min(get_total_active_balance(state) // increment, n_sat)
corr_num = 5 * n_sat - 3 * n_active
corr_den = 2 * n_sat
# 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, Gwei(offset * corr_num // corr_den))
# 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)
proposer_offset = Gwei(proposer_offset * corr_num // corr_den)
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, Gwei(offset * corr_num // corr_den))process_issuance_offsets is added to process_epoch immediately after process_rewards_and_penalties and before process_effective_balance_updates, so effective balances still hold the epoch’s values and step 2 charges exactly the proposers selected during the epoch.
The cost is the linear proposal’s: 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. The correction multiply-divide keeps offset * corr_num within uint64 for every duty reward, so no wider arithmetic is needed.