SettleMint
ArchitectureSecurityCompliance Modules

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 periodLength and rolling after init
    • Operational signals (BurnExceededWindow event, absorbedBurnExcess counter)
  • Related: Compliance Overview, Supply & Investor Limits, Supply Cap & Collateral

Where this module applies

ConcernIssuanceVolumeLimit
MintingEnforces periodic issuance cap (rolling or fixed window)
Transfers
BurnsReleases 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-limit when 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)

CapabilityWho can callInputsOn-chain effectEmitsNotes
initializeCompliance module registryengine, (maxSupply, periodLength, rolling)Stores config; rejects maxSupply == 0, periodLength == 0, or rolling=true with periodLength > 730One-shot; Initializable guard prevents re-initialization
updateConfigCompliance engine / admin(maxSupply, periodLength, rolling)Updates only maxSupply; reverts if the new config changes periodLength or rolling from init valuesEngine ModuleConfigUpdatedperiodLength and rolling are immutable after init — see Configuration updates
canTransfer (mint path)Compliance enginetoken, from, to, amountReverts with COMPLIANCE_CHECK_REASON_EXCEEDS_QUOTA if current in-window supply + amount > maxSupplyOnly enforced on mints (from == address(0)); transfers and burns pass through
createdCompliance enginetoken, to, amountAdds amount to today's bucket (rolling) or active period total (fixed)Rolling mode uses a fixed 730-slot circular day buffer
destroyedCompliance enginetoken, from, amountReleases capacity; unattributed remainder increments absorbedBurnExcessBurnExceededWindow (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.
}
FieldMutable after init?Notes
maxSupplyYesCan be raised or tightened in place via updateConfig. Tightening does not retroactively revert past mints; it constrains future mints only.
periodLengthNoIn 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.
rollingNoDifferent storage paths — flipping it would invalidate the tracker.

Mint accounting

ModeTrackerRollover 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 TokenSupplyLimit V2) 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:

SignalShapeUse
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, monotonicCumulative 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.

On this page