Design
The main design principle for x/mint module is to provide a predictable, epoch-based token minting mechanism with a
maximum supply cap, tailored for Dchain's requirements.
Design Principles
- Epoch-based rather than block-based: Minting occurs at epoch boundaries (e.g., weekly) rather than every block, providing predictability and reducing computational overhead
- Maximum supply enforcement: Hard cap on total token supply prevents unlimited inflation
- Geometric reduction schedule: Predictable reduction in minting over time
- Flexible distribution: Configurable allocation between staking rewards and community pool
- Burn replacement: Each epoch's mint = scheduled reward + a governance-controlled replacement component that compensates the network for adt burnt during the prior epoch, while still preserving the MaxSupply cap exactly
Minting Flow
End-to-end sequence inside a single AfterEpochEnd(n) invocation, including burn replacement:
Ordering invariants the implementation preserves:
bank.GetSupplyis read before any mint in this hook — soS_currentreflects post-burn, pre-mint state of epochn.- History writes (
Burn[n],Repl[n]) happen after the mint of epochn— on a mint or distribution failure, no partial history is persisted. LastEpochSupplyCollis written last, and storesS_current + cappedTotal— i.e., the supply after this epoch's mint, which becomes next epoch's reference point.
State Management
The mint module maintains several pieces of state:
Params
Configurable parameters that define the minting behavior:
mint_denom: The denomination to mint (e.g., "adt")genesis_epoch_provisions: Initial minting amount per epochepoch_identifier: Which epoch to use (e.g., "week")reduction_period_in_epochs: How often to reduce (e.g., 52 for yearly)reduction_factor: Multiplier for reduction (e.g., 0.8 for 20% reduction)distribution_proportions: How to split minted tokensminting_rewards_distribution_start_epoch: When to start mintingmax_supply: Maximum total supply capburn_replacement_ratio(r): Multiplier in[0, 1]applied to the prior epoch's observed burn to compute this epoch's replacement mint.0disables replacement (default at upgrade);1fully neutralises burns at steady state
Minter
Runtime state tracking current provisions:
epoch_provisions: Current minting amount per epoch (reduces over time)
Historical Tracking
For transparency and auditability:
last_reduction_epoch: Last epoch when reduction was appliedreduction_epochs: Map of epoch number → reduction factor appliedlast_epoch_supply: Single value — adt supply snapshot taken at the end of the previousAfterEpochEnd(after that epoch's mint). Used as the reference point for computing the next epoch's burn diffburn_history: Map of epoch number → adt burnt during that epoch (math.Int as decimal string)replacement_mint_history: Map of epoch number → replacement portion of that epoch's mint (math.Int as decimal string)reward_mint_history: Map of epoch number → reward portion of that epoch's mint (math.Int as decimal string). Supersedes the legacyepoch_provisions_history(which stored scheduled LegacyDec provisions); the v1 → v2 migration backfills this collection from the legacy one viaTruncateIntand then clears the legacy data
burn_history, replacement_mint_history, and reward_mint_history grow one entry per epoch indefinitely. No pruning
is built in; if pruning is added later it should apply uniformly across all three collections.
Reduction Logic
The reduction mechanism ensures decreasing token emission:
if currentEpoch >= lastReductionEpoch + reductionPeriodInEpochs:
oldProvisions = minter.EpochProvisions
newProvisions = oldProvisions * reductionFactor
// Store reduction history
storeReductionEpoch(currentEpoch, reductionFactor)
// Update state
minter.EpochProvisions = newProvisions
lastReductionEpoch = currentEpoch
Per-epoch reward history is recorded separately in reward_mint_history every epoch (see
Hook Logic § Writes), not only at reduction boundaries.
Max Supply Protection
The module ensures the maximum supply is never exceeded:
currentSupply = bank.GetSupply(mintDenom)
if currentSupply >= maxSupply:
// Stop minting
return
provisionedAmount = minter.EpochProvision(params)
if currentSupply + provisionedAmount > maxSupply:
// Mint only up to max supply
provisionedAmount = maxSupply - currentSupply
// Mint the adjusted amount
mintCoins(provisionedAmount)
When burn replacement is active (r > 0), the cap check is applied to the combined reward + replacement total — see
Burn Replacement § MaxSupply Interaction.
Distribution Mechanism
Minted tokens are distributed proportionally:
stakingAmount = mintedCoin * distributionProportions.Staking
communityAmount = mintedCoin * distributionProportions.CommunityPool
// Note: stakingAmount + communityAmount must equal mintedCoin
// This is enforced by params validation (proportions must sum to 1.0)
sendToFeeCollector(stakingAmount) // For validator/delegator rewards
fundCommunityPool(communityAmount) // For community initiatives
Governance Integration
The mint parameters can be updated through governance via MsgUpdateParams. Only the governance module (or configured
authority) can modify parameters, ensuring community control over the minting schedule. The handler at
x/mint/keeper/msg_server.go gates execution on ms.authority == req.Authority and returns govtypes.ErrInvalidSigner
for non-authority callers — this same gate covers burn_replacement_ratio updates with no additional code.
burn_replacement_ratio validation: must be ≥ 0 and ≤ 1. Default value at upgrade is "0".
Burn Replacement
Each epoch's mint is the sum of the scheduled reward and a replacement component that compensates the network for adt burnt during the prior epoch:
where R_n is the scheduled reward, B_{n-1} is the adt burn observed during the previous epoch, and r ∈ [0, 1] is
the governance-controlled BurnReplacementRatio param (default 0, replacement disabled).
Goal
Extend x/mint's per-epoch emission so each epoch's mint = reward + replacement, where:
- Reward continues to follow the geometric-decay schedule (
EpochProvisions × ReductionFactoreveryReductionPeriodInEpochs). - Replacement is an emission component that compensates the network for adt burnt during the prior epoch, scaled by
a governance-controlled ratio
r ∈ [0, 1].
The 100 bn MaxSupply cap remains sacred: replacement only refills under the cap, never above it.
Motivation
x/notary burns adt as a notarisation fee. Without replacement, the mint module is unaware of those burns — it mints
purely on schedule until MaxSupply is reached, so cumulative emission slowly drifts below cap as burns accumulate. The
replacement mechanism allows the network to neutralise (or partially neutralise) burns via governance, without breaking
the MaxSupply contract. Future modules that burn adt are supported automatically — the mint module observes total
supply, not module-specific calls.
Assumed Invariant — Sole Minter
x/mintis the only module that callsbank.MintCoinsfor the adt denom.
This is the load-bearing assumption that makes supply-diff burn observation work. The invariant is enforced by:
- Code comment in
x/mint/keeper/keeper.goadjacent to the burn-calculation logic. - This documentation.
- A defensive runtime clamp: if observed supply diff is negative, burn is treated as zero and an error is logged. This keeps the chain live in the event of an invariant violation but produces a clearly grep-able warning.
If a future requirement introduces a second adt minter, this design must be revisited (likely replaced with active per-burn reporting hooks).
Burn Observation — Supply Diff
At each epoch boundary, the mint keeper computes:
B_n = max(0, S_prev_snapshot - S_current)
where S_prev_snapshot is the adt supply persisted at the end of the previous AfterEpochEnd call (after that epoch's
mint) and S_current is bank.GetSupply(adt) at the start of the current call.
Since x/mint is the only minter, the only way supply can decrease between snapshots is via burns from any module. The mechanism is transparent to the burner — no coordination required, no per-module integration.
Formula
Indexing convention: Subscript n denotes "the epoch that just ended" at the moment AfterEpochEnd(n) fires. The
mint computed in that hook is M_n. The burn observed in that hook (via supply diff against the snapshot persisted at
the end of AfterEpochEnd(n-1)) is B_n — burns that occurred during epoch n. Replacement at end of epoch n is
therefore a function of last epoch's (n−1) data, matching the informal "use last epoch's burn" framing.
The mint per epoch is:
That is, this epoch's replacement is last epoch's total burn, scaled by the ratio. Nothing is subtracted from the
burn; the replacement is a direct, linear function of B_{n-1}. Since B_{n-1} ≥ 0 by construction (negative supply
diffs are clamped at observation time) and r ∈ [0, 1], replacement ≥ 0 always — no max(0, …) clamp is needed.
This is a feed-forward (open-loop) rule: M_n depends only on the external burn signal from the prior epoch and the
deterministic reward schedule, never on its own output. It converges in one epoch when the burn rate stabilises.
Steady-state Behaviour
Assume burns and rewards are constant at B and R:
| Quantity | Value |
|---|---|
Steady-state mint M | R + r·B |
Per-epoch supply change ΔS = M − B | R − (1 − r)·B |
| Convergence to steady state | 1 epoch |
Behaviour at r = 0 | replacement disabled; M = R, ΔS = R − B |
Behaviour at r = 1 | full burn replacement; M = R + B, ΔS = R |
At r = 1 with B > R, total mint exceeds reward and supply continues to grow at the scheduled rate. The MaxSupply
cap bounds cumulative growth in all regimes. The rule is well-defined for any r ∈ [0, 1].
MaxSupply Interaction
The cap is preserved exactly. The new constraint:
capped_total = min(reward_scheduled + replacement_computed, MaxSupply − S_current)
Bookkeeping Rule — Replacement Priority
When reward_scheduled + replacement_computed exceeds available headroom, the recorded split prioritises replacement:
replacement_recorded = min(replacement_computed, capped_total)
reward_recorded = capped_total − replacement_recorded
Edge case: if replacement_computed ≥ capped_total, then reward_recorded = 0 and the entire epoch's mint is
classified as replacement.
This choice is purely a recording convention. All minted coins flow through DistributeMintedCoin to the same
destinations (fee collector + community pool) regardless of internal labelling. The rationale: replacement reflects
observed economic activity (burns actually happened); the reward schedule yields to it at the cap.
Hook Logic
Writes per AfterEpochEnd(n) call
LastEpochSupplyColl(singleton) ←currentSupply + cappedTotal. Supply after this epoch's mint; used as the snapshot for next epoch's burn diff.BurnHistoryColl[n]←B_n(burns observed during epochn). Written every epoch, even on at-cap early return. Zero if no burns or first-run.ReplacementMintHistoryColl[n]←replacement_recorded. Written every epoch. Zero on first-run or when replacement is zero.RewardMintHistoryColl[n]←reward_recorded. Written every epoch. Zero at cap if replacement absorbs the full headroom.
The legacy EpochProvisionsHistoryColl is no longer written — RewardMintHistoryColl supersedes it. The v1 → v2
migration backfills RewardMintHistoryColl from the legacy data (TruncateInt) and clears the legacy collection.
Reads from history: BurnHistoryColl[n-1] (for B_{n-1}). That is the only history read.
First-run trace: On the first post-upgrade call, LastEpochSupplyColl is empty → firstRun = true →
burnThisEpoch = 0, replacement = 0. The hook proceeds to mint the scheduled reward, then writes
BurnHistoryColl[n] = 0, ReplacementMintHistoryColl[n] = 0, RewardMintHistoryColl[n] = rewardScheduled, and
LastEpochSupplyColl = currentSupply + rewardScheduled. From the next call onwards, all history reads succeed.
At-cap trace: When currentSupply ≥ MaxSupply, headroom = 0, cappedTotal = 0. No mint. But all new history
collections are still written (burn = B_n, replacement = 0, reward = 0, snapshot = currentSupply). Queries over
historical epochs remain dense.
See the Minting Flow diagram at the top of this document for the full call sequence inside a single
AfterEpochEnd(n) invocation.
Events
The TypeMintEpochEnd event carries four burn-replacement attributes alongside the existing ones (adding attributes to
an existing event is backward-compatible for downstream indexers, which read attribute by key):
| Attribute | Value |
|---|---|
burnt_last_epoch | B_{n-1} (math.Int as decimal string) |
replacement_amount | replacement_recorded |
reward_amount | reward_recorded |
burn_replacement_ratio | r as decimal string |
Existing attributes (epoch_number, epoch_provisions, amount, total_supply_after, max_supply,
last_reduction_epoch) are unchanged. amount continues to be the total minted (now reward + replacement).
A separate log line at Error level fires on negative supply diff. No event is emitted for the warning — it is
purely an operator-facing signal.
Edge Cases
r = 0: replacement disabled; behaviour identical to the pre-burn-replacement schedule.B_{n-1} = 0: no burns; replacement =r · 0 = 0.- First post-upgrade epoch: end of first epoch: snapshot persisted (after the epoch's reward mint),
BurnHistoryColl[n]=0,ReplacementMintHistoryColl[n]=0. Replacement becomes possible from the second post-upgrade epoch. - Epoch identifier mismatch: existing early-return; nothing recorded. The snapshot also does not advance; the next valid-identifier epoch's burn diff will capture burns across the elapsed interval.
- Before
MintingRewardsDistributionStartEpoch: existing early-return; burns in this period are not replaced. - Negative supply diff: clamped to 0; error logged.
- At MaxSupply boundary: total clamped to
headroom = MaxSupply − currentSupply. Replacement gets priority in bookkeeping (recorded up tocappedTotal; reward absorbs the shortfall, possibly reaching zero). History still written.
Worked Example (3 epochs)
Parameters: R = 50 (constant, no reduction), r = 0.5, MaxSupply far above current supply. Starting supply (just
before epoch 1's hook fires): 1000. Burns of 70 adt occur during epoch 2 and during epoch 3 (assume zero burns
during epoch 1 for simplicity).
Epoch 1 (first run) — snapshot in: absent · S_curr = 1000 · B_n = 0 (first-run skips diff) · B_{n-1} = — ·
replacement = 0 · mint = 50 · supply after = 1050 · writes: Burn[1]=0, Repl[1]=0.
Epoch 2 — snapshot in: 1050 · S_curr = 980 (= 1050 − 70 burnt) · B_n = 70 · B_{n-1} = 0 (from Burn[1]) ·
replacement = 0.5 · 0 = 0 · mint = 50 · supply after = 1030 · writes: Burn[2]=70, Repl[2]=0.
Epoch 3 — snapshot in: 1030 · S_curr = 960 (= 1030 − 70 burnt) · B_n = 70 · B_{n-1} = 70 (from Burn[2]) ·
replacement = 0.5 · 70 = 35 · mint = 85 · supply after = 1045 · writes: Burn[3]=70, Repl[3]=35.
Replacement at end of epoch n always uses B_{n-1} read from the history collection, not from the current epoch's
freshly-observed burn. The current epoch's burn observation is stored for use one epoch later.
At epoch 3 the chain mints 85 (50 reward + 35 replacement) on top of supply that dropped to 960 — net supply moves to
1045, recovering some of the burn-driven loss. With sustained constant burns of 70 and r = 0.5, the system converges
in one further epoch to a steady-state mint of 50 + 0.5·70 = 85 per epoch, with net per-epoch supply change
85 − 70 = 15 (matching R − (1 − r)·B = 50 − 0.5·70 = 15).
Migration
Consensus version bumps from 1 → 2 when burn replacement is introduced. The handler in x/mint/migrations/v2/migrate.go
does two things:
- Sets
burn_replacement_ratioto0(the safe default). The mechanism is inert until governance later raisesr, so chain emission is byte-identical to v1 immediately after upgrade. - Backfills
RewardMintHistoryCollfrom the legacyEpochProvisionsHistoryColl, converting eachLegacyDecscheduled-provision entry tomath.IntviaTruncateInt, then clears the legacy collection. At v1 there is no replacement, so scheduled provisions equal actually-minted reward modulo truncation — the copy is lossless under v1 semantics and preserves the historical reward series under the new key.
The migration intentionally does not seed LastEpochSupplyColl. The first post-upgrade AfterEpochEnd detects the
missing snapshot, records bank.GetSupply(adt) after minting reward, and skips replacement for that epoch. Replacement
becomes operational from the second post-upgrade epoch onwards, but only when governance later sets r > 0.
This intentionally drops any burns that occur between the upgrade height and the first post-upgrade epoch end. The
trade-off was accepted because the alternative (seeding the snapshot at migration time) adds complexity for negligible
benefit when r = 0 at activation anyway.
Genesis
GenesisState includes the burn-replacement collections so chain export/import is round-trip symmetric:
last_epoch_supply— optional; if absent, the firstAfterEpochEndafter import seeds it.burn_history— repeated entries of(epoch_number, amount).replacement_mint_history— repeated entries of(epoch_number, amount).reward_mint_history— repeated entries of(epoch_number, amount).
InitGenesis writes each field if present. ExportGenesis exports all four.
Queries
History collections are exposed via a unified range-query surface — see x/mint/docs/06_client.md for the live CLI,
gRPC, and REST contract. BurnHistory, ReplacementMintHistory, RewardMintHistory, and ReductionFactor share a
common request shape (epoch_number + cosmos.base.query.v1beta1.PageRequest); LastEpochSupply is parameterless.