Skip to content

Creation-only collection rules: ownership times, supply caps, backed paths, forceful transfers, pool creation, and post-transfer EVM checks.

Invariants are rules set when a collection is created that can never be changed or removed. They give holders guarantees that no manager action can undo.

Shape

ts
const invariants: CollectionInvariants<bigint> = {
  noCustomOwnershipTimes: false,
  maxSupplyPerId: '0',
  cosmosCoinBackedPath: undefined,
  noForcefulPostMintTransfers: false,
  disablePoolCreation: false,
  evmQueryChallenges: [],
};
proto
message CollectionInvariants {
  bool noCustomOwnershipTimes = 1;
  string maxSupplyPerId = 2;
  CosmosCoinBackedPath cosmosCoinBackedPath = 3;
  bool noForcefulPostMintTransfers = 4;
  bool disablePoolCreation = 5;
  repeated EVMQueryChallenge evmQueryChallenges = 6;
}
FieldTypeEffect
noCustomOwnershipTimesboolEvery ownershipTimes in balances, collection approvals, and user approvals must be the full range [{ start: 1, end: 18446744073709551615 }]
maxSupplyPerIdUintAfter every transfer, no token ID may have total supply above this. 0 disables.
cosmosCoinBackedPathCosmosCoinBackedPathThe collection is backed 1:1 by an x/bank coin. One path per collection. See Backed Minting.
noForcefulPostMintTransfersboolCollection approvals whose fromListId is not exactly "Mint" cannot set overridesFromOutgoingApprovals or overridesToIncomingApprovals
disablePoolCreationboolx/gamm rejects pools that include this collection's assets
evmQueryChallengesEVMQueryChallenge[]Read-only EVM calls run after all balance updates. Any failure reverts the transfer. Up to 10.

Ask your agent:

text
Create a collection with a hard cap of 1 per token ID, no custom ownership times, and no forceful post-mint transfers, locked as invariants.

The MCP builder tools (set_invariants) produce the objects on this page.

How It Works

Set invariants only in the message that creates the collection (MsgCreateCollection, or MsgUniversalUpdateCollection with collectionId: "0"). Including invariants in an update through MsgUniversalUpdateCollection fails with ErrInvariantsImmutable. Read them back from TokenCollection.invariants.

noCustomOwnershipTimes

Turn this on when you do not need time-bound ownership, which is most collections. The chain then rejects any balance, approval, or transfer that uses a partial ownership time range, on every level.

maxSupplyPerId

A hard cap on supply per token ID, checked after every transfer. It is a sanity check on top of mint approval design, not a replacement for it. See Minting and Supply.

noForcefulPostMintTransfers

With this on, no post-mint collection approval can bypass user-level approvals, so forceful revocation and forced distribution are forbidden. The manager can still freeze transfers by removing or restricting collection approvals when their permissions allow it. Mint approvals are exempt because the Mint address has no approvals to override. The check runs both when approvals are set and when a transfer executes.

disablePoolCreation

Pool creation in gamm fails for this collection's denoms.

evmQueryChallenges

Post-transfer invariants use the same challenge structure as the approval criterion but run once, after all balance changes in the transfer, and can see every recipient. Use them for global rules such as a maximum holder count, a per-address balance cap, or a compliance contract that inspects the resulting state.

ts
const evmQueryChallenges: EVMQueryChallenge<bigint>[] = [
  {
    contractAddress: '0x5fbdb2315678afecb367f032d93f642f64180aa3',
    calldata: '70a08231000000000000000000000000$collectionId',
    expectedResult: '0000000000000000000000000000000000000000000000000000000000000001',
    comparisonOperator: 'eq',
    gasLimit: '250000',
    uri: '',
    customData: '',
  },
];

Placeholders in the invariant context:

PlaceholderReplaced with
$initiatorInitiator's address as hex
$senderSender's address as hex
$recipientFirst recipient only
$recipientsEvery recipient, each ABI-padded to 32 bytes, concatenated with no separator (two recipients = 128 hex characters)
$collectionIdCollection ID as a 32-byte padded uint256

Gas: default 250000 per query when gasLimit is 0, maximum 500000 per query, maximum 2500000 across all invariants on the collection, and at most 10 invariants. A contract that reads collection state through the Tokenization Precompile needs roughly 145000 gas for one getCollectionStats call, so leave headroom.

Edit this page on GitHub