Skip to main content
Version: Next

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.

CapabilityGates
enable_metadataMsgSetDenomMetadata
enable_force_transferMsgForceTransfer. Inert since v1.1: the message is always rejected.
enable_burn_fromBurning from an address other than the sender's. Inert since v1.1: the path is always rejected.
enable_admin_sudo_mintMinting on behalf of a denom by the module authority
enable_community_pool_fee_fundingSending 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.

ModuleCheckInterval
liquidityIn-memory state invariantsEvery block
liquidityReserve, farm, and fee account balances reconciled against bank balances and LP supplyEvery 10,000 blocks
incomeEvery yield pool holds at least its total unpaid liability, including holders never checkpointed, plus active NFT earmarksEvery 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.

ModuleCap
Token factory100,000 denoms, 100,000 pool policy records, 100,000 blacklisted addresses
Liquidity10,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 market10,000 active listings, 2,048 bytes per listing, 32 accepted denoms per class
Income1,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_admins list 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.

  1. A blacklist entry is absolute. No role, including gov, can send or receive a blacklisted denom.
  2. 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.
  3. A consumed authorization is final. No role can un-consume one or revive a used sequence number.
  4. Sequence numbers are never reused. Pruning an expired authorization does not return its order_seq to the available space.
  5. The output of a swap always goes to the holder. The executor cannot redirect it.
  6. A class's metadata immutability flag cannot be changed. It is fixed at creation and no message alters it.
  7. Pool close and retirement are one-way. closing clears every pending fee and ownership change, and retirement deletes the pool.
  8. 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 DeliverTx will, so a dry run is meaningful.