Minimal Issuance Reform: Floored Offset Tapering
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.
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.