Skip to content

feat(rewarding): implement IIP-62 productive inflation (curve + 80/20 split)#4846

Open
CoderZhi wants to merge 4 commits into
masterfrom
inflation
Open

feat(rewarding): implement IIP-62 productive inflation (curve + 80/20 split)#4846
CoderZhi wants to merge 4 commits into
masterfrom
inflation

Conversation

@CoderZhi

@CoderZhi CoderZhi commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements IIP-62 productive inflation: post-activation, a per-block protocol mint replaces the fixed block/epoch reward, split 80/20 between the staker reward pool and the Machina DAO along the 5‑20‑Half disinflation curve. The curve and split follow the proposal; the notes below cover where this implementation makes decisions the spec does not pin down.

Implementation decisions (beyond IIP-62)

  • State is derived, not stored. InflationState persists only the three year‑boundary fields (outstandingSupplyAtYearStart, currentInflationBps, currentYearIndex). The running supply, cumulative mint, per‑year remainder, and epoch surplus are recomputed on demand (deterministic from genesis + height), so the record is written once per year instead of every block — archive growth for the "inf" key stays bounded. OutstandingSupply / PostActivationMinted derive their answer at the query height.
  • Split rounding. SplitMint is stateless: staker = ⌊mTotal·bps/denom⌋, Machina = mTotal − staker. Conserves mTotal exactly every block; the sub‑Rau truncation biases toward Machina (well under a micro‑IOTX over the chain's lifetime), so no per‑block dust accumulator is carried.
  • Epoch reward. Funded by the per‑block staker surplus over the (clamped) block reward, summed in closed form over each epoch's block range (EpochInflationSurplus) rather than a stored accumulator. Assumes the block reward is constant within an epoch — holds post‑Wake, where IIP-62 activates.
  • Block‑reward clamp. effective_block_reward = min(blockReward, mStaker) so grants track down past the Y12+ floor and the fund never underflows.
  • Activation reuses the existing ToBeEnabledBlockHeight gate (no new hardfork field). OutstandingSupplyAtActivation is a genesis‑pinned snapshot of circulating supply at activation‑1 (produced by a forthcoming snapshot subcommand).
  • Machina credit is a pure balance credit (AddBalance) — no EVM call into the recipient, which must tolerate passive accrual (a multisig / Safe works natively; a contract relying on receive() for internal accounting would not see it).
  • Mint‑only supply. IoTeX deposits the EIP‑1559 base fee into the rewarding pool (not burned), so OutstandingSupply only grows; there is no burn‑side debit.

Tests

go test ./action/protocol/rewarding/... — curve spec values (§1.1 / §1.3) and 20‑year table walk; the per‑block and epoch derivations cross‑checked against brute‑force per‑block sums (single‑year, year‑final, straddle, pre‑activation); fund‑invariant and reorg‑safety walks; and end‑to‑end OutstandingSupply / PostActivationMinted getter coverage.

Follow-ups (not in this PR)

Snapshot subcommand for OutstandingSupplyAtActivation; e2e activation + replay test across a compressed Y1→Y2 boundary; Machina DAO multisig (externally generated — this PR only credits the balance).

Dependency

The paired iotex-proto PR adding the INFLATION_MINT_{STAKER,MACHINA} transaction‑log types must merge first; go.mod pins to it.

🤖 Generated with Claude Code

CoderZhi and others added 3 commits June 1, 2026 11:17
- Pre-stage Y1 yearMintRemainder in initInflationState so the final-block
  flush is non-zero (the boundary branch only fires from Y2+).
- Cache parsed MachinaDaoAddress on *Protocol; mintAndAllocate no longer
  reparses the bech32 string each block.
- Document Machina credit semantics: pure AddBalance, no receive() /
  fallback() runs - DAO contract must tolerate passive balance accrual.
- Add TestMintAndAllocate_FundInvariant covering high-mint and floor
  regimes; default MachinaDaoAddress in testProtocol harness.
- Drop unused decrementOutstandingSupply / MachinaDaoAddr helper; rewrite
  base-fee comments to reflect IoTeX's deposit-to-pool reality (not burn).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add TestMintAndAllocate_ReorgSafe_YearBoundary: walk Y1 to its last block,
snapshot (inflation, fund, machina) state, execute the Y2-first block, then
restore pre-boundary state and re-execute. Assert post-states are byte-
identical. Covers the validator-orphan case where the boundary block is
re-applied; relies on persisted currentYearIndex to make the boundary
branch re-fire deterministically.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@envestcc envestcc added this to the v2.5 milestone Jun 15, 2026
// IsYearBoundary reports whether height is the first block of a new Year ≥ 2.
// The activation block itself is the start of Year 1 and returns false here — its
// initialization is the activation hook, not a year transition.
func IsYearBoundary(activation, blocksPerYear, height uint64) bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems not used

@CoderZhi CoderZhi marked this pull request as ready for review June 17, 2026 06:34
@CoderZhi CoderZhi requested a review from a team as a code owner June 17, 2026 06:34
… year-boundary writes

InflationState was putState'd every block because several fields mutated
per-block. They are all deterministic, so store only the year-boundary fields
and derive the rest, dropping the per-block _inflKey write (archive growth for
this key goes from per-block to once/year).

- SplitMint is now stateless: staker = floor(mTotal*bps/denom), Machina =
  mTotal - staker. Conserves mTotal exactly; drops the dustStaker/dustMachina
  accumulators (sub-Rau per-block bias, negligible over the chain's lifetime).
- outstandingSupply, postActivationMinted, epochRemainderAccumulator, and
  yearMintRemainder are no longer stored. OutstandingSupply/PostActivationMinted
  are derived via CumulativeMinted (snapshot + query height from p.state); the
  year-end remainder is recomputed on the final block; the epoch surplus is
  derived in closed form via EpochInflationSurplus over the epoch's block range.
- mintAndAllocate persists InflationState only on year-boundary blocks; the
  year-start snapshot is recomputed via the genesis recurrence
  (ComputeYearStartSupply), which is reorg-deterministic.
- proto: reserve fields 1,3,6,7,8,9; InflationState keeps only
  outstandingSupplyAtYearStart, currentInflationBps, currentYearIndex.

Note: EpochInflationSurplus assumes a constant block reward across an epoch
(holds post-Wake, where IIP-62 activates).

Tests: brute-force cross-checks for CumulativeMinted and EpochInflationSurplus
(single-year, year-final, straddle, pre-activation); end-to-end getter coverage
via a fixed-height StateReader; fund-invariant and reorg tests updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants