Issuance Volume Limit
IssuanceVolumeLimit module — periodic token-unit issuance quota with newest-first burn attribution and period-mode immutability.
Purpose: Reference for the periodic token-unit issuance quota module.
- Doc type: Reference
- What you'll find here:
- When to use IssuanceVolumeLimit vs sibling modules
- Interface table and config shape
- Mint and burn accounting rules, including the newest-first (LIFO) burn-attribution rule
- Immutability guardrails for
periodLengthandrollingafter init - Operational signals (
BurnExceededWindowevent,absorbedBurnExcesscounter)
- Related: Compliance Overview, Supply & Investor Limits, Supply Cap & Collateral
Where this module applies
| Concern | IssuanceVolumeLimit |
|---|---|
| Minting | Enforces periodic issuance cap (rolling or fixed window) |
| Transfers | — |
| Burns | Releases capacity within the window (newest-first / LIFO) |
| Forced transfers | — |
When to use this module
Use IssuanceVolumeLimit when you need to answer "how many token units may be issued in a given window?" — for example:
- Dilution control (no more than 100k tokens issued per quarter)
- Vesting / distribution rate limits
- Tranche release programs
- Operational throttling
It is not:
- An absolute outstanding-supply cap — use Capped
- A fiat-denominated fundraising cap — use
capital-raise-limitwhen that module ships - A mark-to-market outstanding value control
- A backing/collateral enforcement — use Collateral
The module is one of several narrower replacements for the broader TokenSupplyLimit V2, which will be sunset.
Interface (capabilities)
| Capability | Who can call | Inputs | On-chain effect | Emits | Notes |
|---|---|---|---|---|---|
initialize | Compliance module registry | engine, (maxSupply, periodLength, rolling) | Stores config; rejects maxSupply == 0, periodLength == 0, or rolling=true with periodLength > 730 | — | One-shot; Initializable guard prevents re-initialization |
updateConfig | Compliance engine / admin | (maxSupply, periodLength, rolling) | Updates only maxSupply; reverts if the new config changes periodLength or rolling from init values | Engine ModuleConfigUpdated | periodLength and rolling are immutable after init — see Configuration updates |
canTransfer (mint path) | Compliance engine | token, from, to, amount | Reverts with COMPLIANCE_CHECK_REASON_EXCEEDS_QUOTA if current in-window supply + amount > maxSupply | — | Only enforced on mints (from == address(0)); transfers and burns pass through |
created | Compliance engine | token, to, amount | Adds amount to today's bucket (rolling) or active period total (fixed) | — | Rolling mode uses a fixed 730-slot circular day buffer |
destroyed | Compliance engine | token, from, amount | Releases capacity; unattributed remainder increments absorbedBurnExcess | BurnExceededWindow (when remainder > 0) | Rolling burns walk newest-first (LIFO) — see Burn attribution |
Configuration
struct IssuanceVolumeLimitConfig {
uint256 maxSupply; // Per-period cap in raw token units (with token decimals). Must be > 0.
uint256 periodLength; // Window length in days. Must be > 0. Locked after init.
bool rolling; // true = rolling window, false = fixed window. Locked after init.
}| Field | Mutable after init? | Notes |
|---|---|---|
maxSupply | Yes | Can be raised or tightened in place via updateConfig. Tightening does not retroactively revert past mints; it constrains future mints only. |
periodLength | No | In fixed mode there is no per-day detail to reinterpret against a new boundary. In rolling mode an expand can instantly block the next mint. To run with a different period, deploy a new module instance. |
rolling | No | Different storage paths — flipping it would invalidate the tracker. |
Mint accounting
| Mode | Tracker | Rollover behavior |
|---|---|---|
Rolling (rolling=true) | Fixed 730-slot circular daily buffer. Mints accumulate into today's slot. | Slots older than periodLength days naturally age out of the read window as time advances. |
Fixed (rolling=false) | Single (periodStart, totalSupply) pair. Mints accumulate into the active period. | When block.timestamp >= periodStart + periodLength * 1 days, the period has rolled over; the next mint starts a new period. |
Active period — precise definition (fixed mode)
The active period is the half-open interval [periodStart, periodStart + periodLength * 1 days). At exactly t == periodStart + periodLength * 1 days, the old period is no longer active, and a new period has not yet been initialized — periodStart is only updated on the next mint that triggers the tracker update. See the NatSpec on _isFixedPeriodActive for the exact semantics.
Burn attribution
In rolling mode, burns release capacity using newest-first (LIFO) attribution. The algorithm walks days backwards from currentDay, consuming the newest non-empty daily slot first until the burn amount is fully attributed.
Rationale (captured in the contract NatSpec):
- Deterministic and easy to explain to auditors.
- Fulfills the "burns release capacity" promise — a current-day-only rule (like the cousin
TokenSupplyLimitV2) would not. - Recent issuance is the most flexible part of the budget: it has had the least time to settle into downstream accounting, distribution, or regulatory reporting, so it is the most defensible to retract.
- FIFO (oldest-first) would be counter-productive — oldest in-window slots are about to roll out of the window naturally on the next aging tick, so subtracting from them yields zero practical headroom.
Overflow behavior
When a burn cannot be fully attributed (tracker empty, burn exceeds all in-window supply, or fixed mode outside an active period), the unattributed remainder is silently dropped from tracker accounting — the burn itself succeeds at the token layer; the module simply cannot release capacity it never tracked (e.g., tokens minted before the current rolling window).
Two operational signals expose this:
| Signal | Shape | Use |
|---|---|---|
BurnExceededWindow event | (uint256 indexed attempted, uint256 indexed absorbed, uint256 indexed windowSpan) | Indexed off-chain; trip alerts when the absorbed fraction is non-trivial |
absorbedBurnExcess() | uint256 view, monotonic | Cumulative on-chain counter of all absorbed remainders; never decremented |
Configuration updates
Only maxSupply may be updated in place after initialization. Any attempt to change periodLength or rolling via updateConfig reverts with InvalidConfig("... cannot be changed; redeploy module"). The compliance engine's standard ModuleConfigUpdated event still fires — the module does not suppress it — but the module itself does not mutate its tracker.
Re-submitting an unchanged config (or a same-value maxSupply) is accepted as a no-op: no tracker mutation, no module-level event.
The dapp UI mirrors this: when the module is already enabled, periodLength and rolling inputs are disabled with helper copy explaining that the operator must remove and re-add the module to change them.
Rollout posture
The module ships under the __experimental__ suffix with the typeId keccak256("issuance-volume-limit__experimental__") and is gated behind the issuanceVolumeLimit feature flag (see kit/contracts/ignition/config/features.ts). The suffix and flag will be dropped in a future migration once the module graduates from experimental status.
Supply & Investor Limits
TokenSupplyLimit and InvestorCount modules — time-based and rolling supply caps with currency conversion, and unique holder limits with per-country granularity.
Transfer Approval
TransferApproval module — pre-authorization workflow with expiry, one-time use, and identity-based exemptions for regulated transfer control.