immutability
Lock collection permissions to make properties permanently immutable or permanently permitted
Transferability & Update Rules
Instruction text for agents that use the Transferability & Update Rules skill, loaded by bb dev skills immutability and the MCP get_skill_instructions tool.
Try it
Paste this into Claude Code, Codex, or Cursor with the BitBadges MCP server wired. The agent loads the skill, builds, verifies, and hands back a link to review and sign.
Load the immutability skill and build me one: lock collection permissions to make properties permanently immutable or permanently permitted. Use the session tools, run validate, review, and simulate in parallel, fix anything critical, then call get_review_url and give me the link.From a shell, bb dev skills immutability prints the same instructions.
Summary
Controls whether collection properties can be changed after creation.
- Two states: FROZEN (permanentlyForbiddenTimes: FOREVER) or NEUTRAL (empty [])
- NEUTRAL [] = manager can update now and can freeze it later. Use this for editable fields.
- FROZEN = permanent and irreversible. Use for fields that should never change.
- Avoid permanentlyPermittedTimes: it permanently prevents locking. Almost never needed.
- canUpdateCollectionApprovals: controls transfer rule mutability
- Security: If manager can update Mint approvals, they can mint unlimited tokens
- Default to frozen unless user requests updatable
- List IDs in permissions: only use reserved IDs ("All", "Mint", "!Mint", direct "bb1..." addresses)
- permanentlyForbiddenTimes: [{ "start": "1", "end": "18446744073709551615" }] = frozen forever
Instructions
Transferability & Update Rules Configuration
When configuring collection permissions for transferability and update rules, you must follow these critical requirements:
Critical Permission Rules
canUpdateCollectionApprovals:
- Critical: Controls whether transfer rules (approvals) can be changed after creation
- Security risk: If the manager can update approvals from the "Mint" address, they can mint any amount
- Default: Should be forbidden (frozen) for collections where transfer rules should be locked
- Format: Uses CollectionApprovalPermission format (see below)
CollectionApprovalPermission Format
{
"fromListId": "All",
"toListId": "All",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalId": "All",
"permanentlyPermittedTimes": [],
"permanentlyForbiddenTimes": [{ "start": "1", "end": "18446744073709551615" }]
}Key Fields:
- List IDs (fromListId, toListId, initiatedByListId): Use only reserved list IDs:
- "All": Any address
- "Mint": Mint address
- "!Mint": Everything except Mint
- "bb1...": Direct address
- "!bb1...": Everything except the specific address
- "bb1abc:bb1xyz": Colon-separated addresses
- Do not use: Custom list IDs, only reserved IDs or direct addresses
- approvalId: "All" to restrict all approvals, or a specific approvalId string
Example: Immutable Transfer Rules (All Frozen)
{
"collectionPermissions": {
"canUpdateCollectionApprovals": [{
"fromListId": "All",
"toListId": "All",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalId": "All",
"permanentlyForbiddenTimes": [{ "start": "1", "end": "18446744073709551615" }],
"permanentlyPermittedTimes": []
}]
}
}Example: Restricting Only Mint Approvals
{
"canUpdateCollectionApprovals": [{
"fromListId": "Mint",
"toListId": "All",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalId": "All",
"permanentlyForbiddenTimes": [{ "start": "1", "end": "18446744073709551615" }],
"permanentlyPermittedTimes": []
}]
}Example: Restricting Specific Approval ID
{
"canUpdateCollectionApprovals": [{
"fromListId": "All",
"toListId": "All",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalId": "mint-approval",
"permanentlyForbiddenTimes": [{ "start": "1", "end": "18446744073709551615" }],
"permanentlyPermittedTimes": []
}]
}Empty Permission Arrays: critical
Important: If a permission entry has both permanentlyPermittedTimes and permanentlyForbiddenTimes as empty arrays, the entire permission entry is redundant and should be replaced with an empty array.
For ActionPermission (canDeleteCollection, canArchiveCollection, etc.):
- Wrong:
"canArchiveCollection": [{ "permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [] }] - Correct:
"canArchiveCollection": []
For TokenIdsActionPermission (canUpdateTokenMetadata, canUpdateValidTokenIds):
- Wrong:
"canUpdateTokenMetadata": [{ "tokenIds": [...], "permanentlyPermittedTimes": [], "permanentlyForbiddenTimes": [] }] - Correct:
"canUpdateTokenMetadata": []
For CollectionApprovalPermission (canUpdateCollectionApprovals):
- If both time arrays are empty, use empty array:
"canUpdateCollectionApprovals": []
Permission Presets
Three common permission configurations:
1. Fully Immutable: Everything frozen. Nothing can change after creation.
- All permissions set to permanentlyForbiddenTimes: FOREVER
- Use when: the collection should never change
2. Manager Controlled: Manager can change everything except delete.
- canDeleteCollection: frozen
- Everything else: NEUTRAL [] (editable now, can be frozen later)
- Use when: the manager needs full control (issuer-controlled tokens, evolving collections)
3. Locked Approvals (recommended default): Approvals and supply frozen, metadata editable.
- canDeleteCollection: frozen
- canUpdateStandards: frozen
- canUpdateManager: frozen
- canUpdateValidTokenIds: frozen
- canUpdateCollectionApprovals: frozen
- canUpdateCollectionMetadata: NEUTRAL []
- canUpdateTokenMetadata: NEUTRAL []
- canArchiveCollection: NEUTRAL []
- canUpdateCustomData: NEUTRAL []
- Use when: supply and rules should be immutable but metadata needs updates
Understanding permission states:
- Empty array [] = NEUTRAL (manager can update now and can lock it later, preserves maximum flexibility)
- permanentlyForbiddenTimes: FOREVER = FROZEN (can never be changed, permanent and irreversible)
- permanentlyPermittedTimes: FOREVER = PERMANENTLY ALLOWED (can never be frozen, almost never needed)
Important: For editable fields, always use NEUTRAL (empty []) instead of permanentlyPermittedTimes. Neutral gives the same current behavior (manager can update) but preserves the option to freeze it later. Only use permanentlyPermittedTimes if the user explicitly requests a guarantee that a field can never be locked.
Security Considerations
- Mint Transfer Rules: If canUpdateCollectionApprovals is allowed for Mint, the manager could mint unlimited tokens
- Post-Mint Transfer Rules: If post-mint transfer rules can be updated, the manager could change transferability
- Best Practice: Default to locked (frozen) transfer rules unless user explicitly requests updatable rules
- Only allow updates for dynamic collections where the user explicitly requests flexibility
Common Mistakes
- Don't use custom list IDs in permissions, only reserved IDs: "All", "Mint", "!Mint", or direct bb1... addresses.
- Don't leave both permanentlyPermittedTimes and permanentlyForbiddenTimes as empty arrays in a permission entry, this is redundant. Replace the entire entry with an empty array [].
- Don't forget that unfrozen Mint approval permissions means the manager can mint unlimited tokens, freeze canUpdateCollectionApprovals for Mint if supply should be fixed.
- Don't confuse empty permission array [] (neutral/unset) with a frozen permission, empty means the field is still updatable.