Minimal Issuance Reform: Floored Quadratic Offset Tapering
tl;dr
- The quadratic offset taper brings the net yield to zero tangentially at a 50% staking ratio — a smooth pinch-off rather than the linear taper’s kink;
- This variant applies the \(\tfrac{1}{64}\) floor scaling of the floored linear taper — halved from \(\tfrac{1}{32}\) because the quadratic taper doubles
BASE_REWARD_FACTOR, so the two floors are the same absolute size — to the quadratic offset, leaving a fixed \(\tfrac{1}{64}\) slice of every reward and a small permanent issuance floor of \(\tfrac{1}{64}\) of the untapered issuance, reached at the saturation ratio; - Because the quadratic taper meets saturation with zero slope, the floored curve joins the floor \(\tfrac{1}{64}y(f)\) with matching slope — a smooth descent into the floor, where the floored linear taper meets it at an angle;
- The change to the quadratic proposal is one new constant and a single extra subtraction per duty; the quadratic correction factor and offset helper are reused unchanged.
Introduction
Two earlier variations on the linear offset taper pulled in different directions. The quadratic taper made the pinch-off smoother, replacing the linear offset with a quadratic one so the net yield reaches zero with zero slope at the saturation ratio \(f_\text{sat} = 50\%\). The floored linear taper instead kept issuance positive everywhere, scaling the offset down to \(\tfrac{63}{64}\) of its value to leave a small permanent floor.
This note combines the two: it applies a \(\tfrac{1}{64}\) floor to the quadratic offset. The floor fraction is halved from the floored linear taper’s \(\tfrac{1}{32}\) because the quadratic taper runs at twice the BASE_REWARD_FACTOR, so a \(\tfrac{1}{64}\) slice here is the same absolute size of issuance as a \(\tfrac{1}{32}\) slice there. The natural question is whether the floor disturbs the quadratic taper’s defining property — its smooth, tangential arrival at saturation. It does not. In fact the tangency is exactly what makes the floored version smooth: the net yield descends and joins the floor curve with matching slope, with no kink at the transition. Everything else carries over from the quadratic proposal unchanged.
The Issuance Floor
Recall the quadratic offset fraction, which deducts a fraction \(q(f)\) of each validator’s ideal reward, leaving net yield \((1-q(f))\,y(f)\):
\[q(f) = \tfrac{1}{2}(2f)^{3/2}(5 - 6f), \qquad q(f_\text{sat}) = 1,\]
clamped at \(q = 1\) for \(f \ge f_\text{sat}\). It was constructed so that the net yield reaches zero tangentially at saturation — equivalently, so that \(q'(f_\text{sat}) = 0\). The clamp at \(q = 1\) already imposes a floor — a floor of zero, holding issuance at zero rather than letting the offset exceed the full reward and turn issuance negative. As in the floored linear proposal, we raise that floor to a small positive value: we hold back a fixed slice — here \(\tfrac{1}{64}\) — and apply only \(\tfrac{63}{64}\) of this offset,
\[q_\text{floored}(f) = \frac{63}{64}\,q(f).\]
A fixed \(\tfrac{1}{64}\) slice of every reward then always escapes the offset, so the net yield levels off at \(\tfrac{1}{64}\,y(f)\) rather than reaching zero. The reward curve, the underlying quadratic offset fraction, and the calibration are all unchanged.
Why the descent stays smooth
Write the floored net yield as the unmodified quadratic net yield plus the recovered slice:
\[n(f) = \left(1 - \tfrac{63}{64}q\right)y = \underbrace{(1 - q)\,y}_{\tilde y(f)} \; + \; \tfrac{1}{64}\,q(f)\,y(f).\]
Just below saturation the slope is \(n'(f_\text{sat}^-) = \tfrac{1}{64}\,y'(f_\text{sat}) - \tfrac{63}{64}\,q'(f_\text{sat})\,y(f_\text{sat})\). The quadratic taper’s tangency condition is precisely \(q'(f_\text{sat}) = 0\), so the second term vanishes and
\[n'(f_\text{sat}^-) = \tfrac{1}{64}\,y'(f_\text{sat}).\]
Beyond saturation \(q\) is clamped at 1 and the net yield is exactly the floor, \(n(f) = \tfrac{1}{64}\,y(f)\), whose slope at saturation is \(n'(f_\text{sat}^+) = \tfrac{1}{64}\,y'(f_\text{sat})\) — the same value. The curve therefore meets the floor with continuous slope: a smooth (tangential) join, not a kink. The floored linear taper has no such property — its offset has non-zero slope at saturation, so its net yield meets the floor at an angle.
Note that the join is smooth but not flat: the floor \(\tfrac{1}{64}y(f)\) slopes gently downward (since \(y\) falls as \(f\) rises), and the net yield curve settles tangentially onto that gentle slope rather than arriving horizontally. The quadratic’s tangential pinch-off to zero becomes a tangential settling onto the floor.
For \(f \ge f_\text{sat}\) the issuance is the floor fraction times the untapered issuance,
\[\tilde i(f) = \frac{1}{64}\,i(f), \qquad f \ge f_\text{sat},\]
in the same form as the floored linear proposal — the floor depends only on the floor scaling and the clamp at saturation, not on the shape of the taper before it. 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. For the compensated \(B = 256\) curve the floor is roughly \(0.067\%\) of supply at saturation, rising to \(0.095\%\) at full-supply staking — the same absolute floor as the floored linear taper’s \(\tfrac{1}{32}\) slice at \(B = 128\), since doubling \(B\) and halving the floor fraction cancel. The alternative \(B = 128\) calibration halves it again, to \(\approx 0.034\%\)–\(0.047\%\).
The Offset Fraction
In absolute terms — working, as the quadratic proposal does, from the total active balance \(D = f\,S\) and the fixed constant \(D_\text{sat}\) (SATURATION_BALANCE), with \(u = D/D_\text{sat}\) — the quadratic 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. Scaling by \(\tfrac{63}{64}\) gives
\[q_\text{floored} = \frac{63}{64}\,q_\text{lin}\cdot\tfrac{1}{2}(5 - 3u).\]
Each duty’s offset is thus the linear offset, multiplied by the quadratic per-epoch correction factor \(\tfrac{1}{2}(5 - 3u)\), and then scaled down to \(\tfrac{63}{64}\) — a \(\tfrac{1}{64}\) slice of every reward always survives. The factors of BASE_REWARD_FACTOR cancel, so \(q_\text{floored}\) is independent of \(B\). At saturation \(q = 1\), so \(q_\text{floored} = \tfrac{63}{64}\) — the full reward less the floor slice — and the tangency condition that flattened the zero-floor curve is now what lands the floored curve smoothly on the floor.
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_\text{floored}\) times the ideal reward for that duty — is exactly as in the quadratic and linear proposals. 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}{64}\) attestation slice.
Consensus Layer Spec
The change to the consensus layer is the quadratic proposal’s, plus a single new constant ISSUANCE_FLOOR_DENOMINATOR \(= 64\) and one extra subtraction per duty. BASE_REWARD_FACTOR is raised and SATURATION_BALANCE is added exactly as before, the get_tapered_offset helper is reused unchanged, and process_issuance_offsets applies the quadratic correction factor \(\tfrac12(5-3u)\) exactly as in the quadratic proposal, then scales each duty’s offset down to \(\tfrac{63}{64}\), removing a \(\tfrac{1}{64}\) slice. Because the quadratic correction already holds the offset at the full reward at and beyond saturation, scaling it places the floor’s onset exactly at \(f_\text{sat}\). The new step uses the same two lookups as the earlier versions: 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(256) |
4× (crossover \(f \approx 30.8\%\)); \(B = 128\) (crossover \(f \approx 21.0\%\), thinner floor) 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 |
ISSUANCE_FLOOR_DENOMINATOR |
— | uint64(64) |
The floor \(\tfrac{1}{64}\): each duty retains at least this fraction of its ideal reward. Halved from the floored linear taper’s \(32\) because \(B\) is doubled, matching the absolute floor. A larger power of two gives a thinner floor |
New Function: the offset helper
Unchanged from the linear and quadratic proposals. 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 floored quadratic offset
Identical to the quadratic step, except each duty’s quadratic offset is scaled down to \(\tfrac{63}{64}\) — offset - offset // ISSUANCE_FLOOR_DENOMINATOR — leaving a \(\tfrac{1}{64}\) slice that is always paid out. The quadratic correction factor \(\tfrac12(5-3u) = (5\,n_\text{sat} - 3\,n)/(2\,n_\text{sat})\) is computed once and applied per duty exactly as before; the floor subtraction is then a single extra operation on the result:
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, as in the quadratic 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
# Apply the quadratic correction, then keep a 1/64 floor slice.
def floored_offset(linear_offset: Gwei) -> Gwei:
quad = uint64(linear_offset) * corr_num // corr_den
return Gwei(quad - quad // ISSUANCE_FLOOR_DENOMINATOR)
# 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, floored_offset(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 = floored_offset(
get_tapered_offset(ideal_proposer_reward, sqrt_active, sqrt_sat)
)
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, floored_offset(offset))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 and quadratic proposals’: 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 linear_offset * corr_num within uint64 for every duty reward, and the floor is a single subtraction per duty, so no wider arithmetic is needed.