Draft

Minimal Issuance Reform: Quadratic Offset Tapering

A one-constraint variation on the linear offset taper: requiring the net yield to reach zero tangentially at a 50% staking ratio turns the linear offset into a quadratic one, for a smooth pinch-off instead of a kink. Everything else in that proposal is unchanged.
Published

June 18, 2026

Modified

June 21, 2026

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_FACTOR natural: \(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.

Figure 1: Consensus layer net yield under the current curve (B=64), the naive quadratic taper (B=64), and the compensated quadratic tapers (B=128, B=256). All reach zero tangentially at \(f_\text{sat} = 50\%\); B=256 crosses the current yield at \(f \approx 30.3\%\).

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.

Figure 2: Annual ETH issuance under the current curve (B=64), the naive quadratic taper (B=64), and the compensated quadratic tapers (B=128, B=256). Both compensated curves peak at \(f \approx 12.8\%\) (B=128 at \(\approx 0.79\%\), B=256 at \(\approx 1.57\%\) of supply) and fall tangentially to zero at \(f = \tfrac12\).

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.

Footnotes

  1. The existing get_beacon_proposer_index generalised to an arbitrary slot in the epoch.↩︎

  2. A pubkey→index lookup, which clients already maintain as a cache.↩︎