Security and governance
Capabilities
Some privileged messages are gated on a capability: a module-wide switch
configured at wiring time. A message whose capability is absent fails with
ErrCapabilityNotEnabled; it is not silently degraded.
| Capability | Gates |
|---|---|
enable_metadata | MsgSetDenomMetadata |
enable_force_transfer | MsgForceTransfer. Inert since v1.1: the message is always rejected. |
enable_burn_from | Burning from an address other than the sender's. Inert since v1.1: the path is always rejected. |
enable_admin_sudo_mint | Minting on behalf of a denom by the module authority |
enable_community_pool_fee_funding | Sending the denom creation fee to the community pool instead of burning it |
The shipped chain enables all five. enable_admin_sudo_mint is wired with a
sudo-admin predicate that always returns false, so the sudo-mint path is not
reachable in the default build.
Two of the five no longer gate a reachable action: enable_force_transfer and
enable_burn_from are inert, because holder asset
protection rejects those paths whatever the
capability says.
Capabilities are deliberately separate from authority. Authority answers who; a capability answers whether that action exists at all on this chain.
Invariants
Two modules run solvency and conservation checks in EndBlock.
| Module | Check | Interval |
|---|---|---|
liquidity | In-memory state invariants | Every block |
liquidity | Reserve, farm, and fee account balances reconciled against bank balances and LP supply | Every 10,000 blocks |
income | Every yield pool holds at least its total unpaid liability, including holders never checkpointed, plus active NFT earmarks | Every 10,000 blocks |
A violation emits an alarm event — liquidity_invariant_violation or
income_invariant_violation, both carrying the height and the error — and
returns no error. That is a deliberate choice: a returned error from
EndBlock aborts FinalizeBlock, so an alarm would itself become a chain halt.
Raising it as an event keeps the chain making progress while making the failure
unmissable to a monitor.
The same income check runs in InitGenesis, where it does fail, because there
is no prior state to continue from.
:::note The deposit invariant is not registered with crisis
nftmarket has a DepositInvariant that the module account balance always
equals the sum of active listing deposits. It is implemented and exercised by
tests, but it is not registered with the crisis module and does not run
automatically.
:::
State limits
Every module caps the size of its collections, and the caps are checked before
any transfer, not after. A swap that would create a 65th fee denom on a record
already holding 64 is rejected with accruedFees untouched.
| Module | Cap |
|---|---|
| Token factory | 100,000 denoms, 100,000 pool policy records, 100,000 blacklisted addresses |
| Liquidity | 10,000 pools; 10,000 farm positions, 1,000 per pool; 100,000 lots, 10,000 per position; 10,000 reward records per pool; 64 denoms per record |
| NFT market | 10,000 active listings, 2,048 bytes per listing, 32 accepted denoms per class |
| Income | 1,000 streams, 50 NFT claims per message |
Liquidity amounts are additionally bounded at 2^256 − 1.
Active listing count and farm record counts are maintained by store counters rather than by counting rows, and the decrements saturate so a counter can never underflow into an invalid state.
Governance
The gov module account is every module's authority. It can:
- Change any module's parameters.
- Move an NFT class's issuer.
- Emergency-pause or close a liquidity pool, and activate an emergency ownership transfer.
- Register or remove an IBC denom, which gates risk-increasing operations on that asset.
- Set the token factory's pool denylist.
- Set the income
income_adminslist through a parameter change.
A denied pool ownership transfer is the exception worth knowing: governance can start the emergency path but the pool owner has no way to cancel it, while governance cannot start the normal path. A cooperative handover and a recovery are separate on purpose.
Immutable rules
These hold regardless of configuration, and no role bypasses them.
- A blacklist entry is absolute. No role, including
gov, can send or receive a blacklisted denom. - A holder's asset cannot be burned or moved without the holder's signature.
No role — not the denom admin, the class issuer, or
gov— can act on another holder's balance or token. See Holder asset protection. - A consumed authorization is final. No role can un-consume one or revive a used sequence number.
- Sequence numbers are never reused. Pruning an expired authorization does
not return its
order_seqto the available space. - The output of a swap always goes to the holder. The executor cannot redirect it.
- A class's metadata immutability flag cannot be changed. It is fixed at creation and no message alters it.
- Pool close and retirement are one-way.
closingclears every pending fee and ownership change, and retirement deletes the pool. - Alarms never halt the chain. An invariant violation is an event, not a failed block.
Requirements on an integrator
- Mirror state, do not infer it. Several messages emit no event, including
MsgSetNFTMetadata, blacklist changes, delegate changes, and params updates. Re-query rather than waiting for the event stream. - Be idempotent. Deduplicate on the order id for swaps, the listing id for the market, and the transaction hash for everything else.
- Reconcile. On-chain events are the source of truth; an off-chain index is a cache and must be rebuildable from them.
- Simulate. The ante chain returns the same rejections in simulation that
DeliverTxwill, so a dry run is meaningful.