Draft

Minimal Issuance Reform: Floored Offset Tapering

A variation on the linear offset taper for those who want issuance never to reach zero: scaling the offset down to 31/32 of its value leaves a small permanent issuance floor — 1/32 of the untapered curve — with everything else in that proposal unchanged.
Published

June 21, 2026

Modified

June 21, 2026

tl;dr

  • The linear offset taper drives staking issuance to zero at a 50% staking ratio and holds it there;
  • This variant scales the linear offset down to \(\tfrac{31}{32}\) of its value, so a fixed \(\tfrac{1}{32}\) slice of every reward always survives — a small permanent issuance floor of \(\tfrac{1}{32}\) of the untapered issuance (roughly \(0.07\)\(0.1\%\) of supply per year), reached at the saturation ratio;
  • The change to the linear proposal is one new constant and a single extra subtraction in the offset step; the offset helper and everything else are untouched.

Introduction

The linear offset taper cancels the staking yield entirely at the saturation ratio \(f_\text{sat} = 50\%\): the offset fraction \(q\) reaches 1, the full reward is offset, and issuance is zero. Beyond saturation \(q\) stays clamped at 1, so issuance remains at a floor of zero.

One possible modification of this proposal would be to impose a tail emissions condition to ensure that the potential exists for positive issuance at all staking ratios. The linear offset removes a fraction \(q_\text{lin}(f)\) of each reward, rising to the whole reward at saturation; in this variant, we simply apply \(\tfrac{31}{32}\) of it,

\[q(f) = \frac{31}{32}\,q_\text{lin}(f).\]

A fixed \(\tfrac{1}{32}\) slice of every reward then always escapes the offset, so net yield and issuance level off at a small positive floor rather than reaching zero. Everything else in the linear proposal — the reward curve, the underlying offset fraction, the calibration — is unchanged.

The Issuance Floor

Recall the linear offset fraction \(q_\text{lin}(f) = \min\!\big((f/f_\text{sat})^{3/2},\,1\big)\), which rises to 1 at saturation and stays clamped there. Scaling it by \(\tfrac{31}{32}\) gives

\[q(f) = \frac{31}{32}\min\!\left(\left(\tfrac{f}{f_\text{sat}}\right)^{3/2},\; 1\right).\]

This reaches its maximum, \(\tfrac{31}{32}\), at \(f=f_\text{sat}\). For \(f \ge f_\text{sat}\) the net yield is the floor fraction times the untapered yield, \(\tfrac{1}{32}\,y(f)\), and issuance is the same fraction of untapered issuance,

\[\tilde i(f) = \frac{1}{32}\,i(f), \qquad f \ge f_\text{sat}.\]

Because the untapered issuance \(i(f) = f\,y(f)\) rises with \(f\), the floor is not flat: issuance reaches its minimum at saturation, then climbs slowly as the staking ratio grows. For the compensated \(B = 128\) curve the floor is roughly \(0.067\%\) of supply at saturation, rising to \(0.095\%\) at full-supply staking.

Figure 1: Consensus layer net yield: the current curve, the unmodified linear taper (zero-issuance floor), and the \(\tfrac{1}{32}\) floored variant, all at \(B = 128\).
Figure 2: Annual ETH issuance for the same curves. The floored variant is indistinguishable from the zero-floor taper until close to saturation, where the zero-floor curve reaches zero and the floored one levels off just above it.

The Offset Fraction

In absolute terms — working, as the linear proposal does, from the total active balance \(D = f\,S\) and the fixed constant \(D_\text{sat}\) (SATURATION_BALANCE) — the offset fraction is

\[q = \frac{31}{32}\min\!\left(\left(\tfrac{D}{D_\text{sat}}\right)^{3/2},\; 1\right).\]

Equivalently, each duty’s offset is the linear offset scaled down to \(\tfrac{31}{32}\), so a \(\tfrac{1}{32}\) slice of every reward always survives. That surviving slice is the issuance floor; scaling the (square-root-clamped) linear offset is what places its onset exactly at saturation.

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. Scaling each duty’s offset independently means the floor is preserved duty-by-duty: a validator only attesting in a given epoch still retains its \(\tfrac{1}{32}\) attestation slice.

Consensus Layer Spec

The change to the consensus layer is the linear proposal’s, plus a single new constant ISSUANCE_FLOOR_DENOMINATOR \(= 32\) and one extra subtraction per duty. BASE_REWARD_FACTOR is raised and SATURATION_BALANCE is added exactly as before, the get_tapered_offset helper — including its square-root clamp at saturation — is reused unchanged, and process_issuance_offsets simply scales each duty’s offset down to \(\tfrac{31}{32}\), removing a \(\tfrac{1}{32}\) slice. Because the helper’s clamp already holds the offset at the full reward beyond saturation, scaling it places the floor’s onset exactly at \(f_\text{sat}\). The new step uses the same two lookups as the linear version: get_beacon_proposer_index_at_slot1 and get_validator_index_by_pubkey.2

Updated Constant and New Constants

Constant Old value New value Notes
BASE_REWARD_FACTOR uint64(64) uint64(128) Calibration, unchanged from the linear proposal (crossover \(f \approx 31\%\))
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
ISSUANCE_FLOOR_DENOMINATOR uint64(32) The floor \(\tfrac{1}{32}\): each duty retains at least this fraction of its ideal reward. A larger power of two gives a thinner floor

Reused Function: the offset helper

Unchanged from the linear proposal. It applies the cubed square-root ratio \((D/D_\text{sat})^{3/2}\) to a reward as three successive uint64 multiply-divides:

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 floored offset

Identical to the linear step, except each duty’s offset is scaled down to \(\tfrac{31}{32}\)offset - offset // ISSUANCE_FLOOR_DENOMINATOR — leaving a \(\tfrac{1}{32}\) slice that is always paid out:

def process_issuance_offsets(state: BeaconState) -> None:
    # sqrt clamp unchanged from the linear proposal: it holds the offset at the
    # full reward beyond saturation, placing the floor's onset exactly at f_sat
    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 - offset // ISSUANCE_FLOOR_DENOMINATOR)

    # 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 = proposer_offset - proposer_offset // ISSUANCE_FLOOR_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)
        decrease_balance(state, index, offset - offset // ISSUANCE_FLOOR_DENOMINATOR)

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 added scaling is a single subtraction per duty.

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.↩︎