smart-token
IBC-backed smart token with 1:1 backing and two required approvals (backing + unbacking)
Smart Token
Instruction text for agents that use the Smart Token skill, loaded by bb dev skills smart-token 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 smart-token skill and build me one: iBC-backed smart token with 1:1 backing and two required approvals (backing + unbacking). 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 smart-token prints the same instructions.
Summary
Required standards: ["Smart Token"]
- Must include cosmosCoinBackedPath in invariants with conversion sideA/sideB
- Must configure at least one alias path (decimals must match IBC denom decimals)
- Must create two required collection approvals (backing + unbacking). Transferable approval is common but optional:
- Backing approval (required): fromListId = backing address, allowBackedMinting: true, mustPrioritize: true
- Transferable approval (optional: include for wrapped assets, omit for vaults/escrows): fromListId = "!Mint", toListId = "All"
- Unbacking approval (required): toListId = backing address, allowBackedMinting: true, mustPrioritize: true
- Do not use fromListId: "Mint", tokens are created via IBC backing, not traditional minting
- Backing addresses are protocol-controlled with auto-set approvals, overridesFromOutgoingApprovals is irrelevant for backing/unbacking (leave unset or false)
- noForcefulPostMintTransfers invariant should be true, smart tokens do not need forceful transfer overrides
- Unbacking fromListId uses "!Mint:backingAddress" syntax (excludes both Mint and backing address, meaning only regular holders can unback)
- Backing address is deterministic, use generate_backing_address tool
- Optional: Add "AI Agent Vault" to standards for AI Prompt tab (display-only)
- Alias path: symbol = base unit (e.g. "uvatom"), denomUnits = display units with decimals > 0 only, each denomUnit must have PathMetadata with a placeholder uri
- PathMetadata only has
{ uri, customData }, do not include image/name/description fields. Set metadata.uri to a placeholder like"ipfs://METADATA_ALIAS_<denom>"and register the image/name/description in the metadataPlaceholders sidecar keyed by that URI
Instructions
Smart Token Configuration
Mental Model: Three Phases
Think about smart tokens in three distinct phases:
- Phase 1: Deposits (Backing): Users send IBC coins (e.g. USDC) to the backing address and receive wrapped tokens 1:1. This is the on-ramp.
- Phase 2: Transferability (While Backed): While tokens exist in the wrapped silo, can users transfer them peer-to-peer? This is the key design decision, transferable for wrapped assets, non-transferable for vaults/escrows.
- Phase 3: Withdrawals (Unbacking): Users send wrapped tokens back to the backing address and receive their IBC coins 1:1. This is the off-ramp. Rate limits, 2FA, and other controls go here.
Each phase maps to at least one collection approval. Design each phase independently, they are orthogonal concerns.
Required Structure
-
Standards: must include "Smart Token"
- "standards": ["Smart Token"]
-
Invariants: must include cosmosCoinBackedPath
json { "standards": ["Smart Token"], "invariants": { "cosmosCoinBackedPath": { "conversion": { "sideA": { "amount": "1", "denom": "ibc/A4DB47A9D3CF9A068D454513891B526702455D3EF08FB9EB558C561F9DC2B701" }, "sideB": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }] } } } } -
Alias Paths: must configure at least one alias path
- The alias path decimals must match the IBC denom's decimals
- This is required for Smart Tokens to function properly
- Use the alias path configuration provided in the skill config
- Alias path denom and symbol must only contain a-zA-Z, _, {, }, and - characters. Never use the raw IBC denom (ibc/...) as the alias path denom, create a new symbol like "wuusdc" or "uwrapped"
- Do not reuse reserved symbols (USDC, ATOM, BADGE, etc.), always prefix with "w" or similar (e.g., wUSDC, wATOM)
Approval System
Smart Tokens require two approvals (backing + unbacking). A third transferable approval is common for wrapped assets but optional for vaults/escrows:
1. Backing Approval (for backing tokens)
This approval allows tokens to be sent from the IBC backing address (backing the tokens).
{
"fromListId": "bb1backingaddress...",
"toListId": "!bb1backingaddress...",
"initiatedByListId": "All",
"approvalId": "smart-token-backing",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalCriteria": {
"mustPrioritize": true,
"allowBackedMinting": true
}
}Note: Backing addresses are protocol-controlled with auto-set approvals. overridesFromOutgoingApprovals is irrelevant and can be omitted.
2. Transferable Approval (optional: for peer-to-peer transfers)
This approval allows tokens to be transferred between users (non-backing addresses). Include for wrapped assets where holders need to trade/transfer. Omit for simple deposit/withdraw vaults or escrows where tokens should not move between users.
{
"fromListId": "!Mint",
"toListId": "All",
"initiatedByListId": "All",
"approvalId": "transferable-approval",
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }]
}3. Unbacking Approval (for unbacking tokens)
This approval allows tokens to be sent to the IBC backing address (unbacking the tokens).
The !Mint:bb1backingaddress... fromListId uses colon-separated exclude syntax, it means "everyone except Mint and the backing address", so only regular holders can unback.
{
"fromListId": "!Mint:bb1backingaddress...",
"toListId": "bb1backingaddress...",
"initiatedByListId": "All",
"approvalId": "smart-token-unbacking",
"tokenIds": [{ "start": "1", "end": "1" }],
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalCriteria": {
"mustPrioritize": true,
"allowBackedMinting": true
}
}Multi-Message Deposit/Withdraw Pattern
For safety, backing approvals enforce that the initiator must be the recipient (deposit) and the initiator must be the sender (withdraw). To deposit to or withdraw for another address, use a multi-message transaction:
Deposit to Another Address (2 msgs in one tx)
- MsgTransferTokens: Deposit to self (from: backingAddress, to: self)
- MsgTransferTokens: Transfer from self to target (from: self, to: targetAddress), uses the transferable approval
Withdraw for Another Address (2 msgs in one tx)
- MsgTransferTokens: Withdraw to self (from: self, to: backingAddress)
- MsgSend (cosmos bank): Send equivalent IBC coins from self to target address
Optional: AI Agent Vault Standard
Add "AI Agent Vault" to the standards array to enable an AI Prompt tab in the frontend. This is display-only and has no impact on on-chain logic.
{
"standards": ["Smart Token", "AI Agent Vault"]
}Optional: DEX Tradability (Liquidity Pools)
Most smart tokens are not tradable on DEX, only add this if the user explicitly requests trading/DEX functionality:
- Add "Liquidity Pools" to standards
- Set invariants.disablePoolCreation to false (override the safe default)
- Must have at least one alias path configured
- Must have a transferable approval (peer-to-peer transfers required for trading)
IBC Backed Minting Rules
- Backing/unbacking approvals: Backing addresses are protocol-controlled with auto-set approvals, overridesFromOutgoingApprovals is irrelevant (leave unset or false). noForcefulPostMintTransfers should be true.
- Unbacking fromListId: Use
!Mint:backingAddresssyntax, colon-separated addresses with!prefix means "everyone except Mint and backing address" (only regular holders can unback) - Use allowBackedMinting: true for IBC backed operations
- Use mustPrioritize: true (required, not compatible with auto-scan)
- Backing Address: Use the backing address as fromListId (not "Mint"): generated deterministically from the IBC denom via generate_backing_address tool
Alias Path Configuration
Must configure at least one alias path. Structure:
{
"aliasPathsToAdd": [{
"denom": "uvatom",
"symbol": "uvatom",
"conversion": {
"sideA": { "amount": "1" },
"sideB": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }]
},
"denomUnits": [{
"decimals": "6",
"symbol": "vATOM",
"isDefaultDisplay": true,
"metadata": { "uri": "ipfs://METADATA_ALIAS_uvatom_UNIT", "customData": "" }
}],
"metadata": { "uri": "ipfs://METADATA_ALIAS_uvatom", "customData": "" }
}]
}Rules:
- symbol = base unit symbol (e.g., "uvatom")
- denomUnits = display units with decimals > 0 only (base decimals 0 is implicit)
- Each denomUnit and the path itself must have PathMetadata of the form
{ uri, customData }. Use a placeholder URI like"ipfs://METADATA_ALIAS_<denom>"/"ipfs://METADATA_ALIAS_<denom>_UNIT". - isDefaultDisplay: true for the primary display unit
- Critical: PathMetadata has exactly two fields,
uriandcustomData. Never addimage,name, ordescriptionhere. Register the name, description, and image for each placeholder URI in themetadataPlaceholderssidecar returned alongside the transaction, the metadata auto-apply flow uploads the off-chain JSON and substitutes the placeholder URIs with real IPFS URIs after deploy.
Cosmos Coin Wrapper (Optional)
For wrapping native Cosmos SDK coins, use allowSpecialWrapping: true and cosmosCoinWrapperPathsToAdd:
{
"cosmosCoinWrapperPathsToAdd": [{
"denom": "uatom",
"symbol": "uatom",
"conversion": {
"sideA": { "amount": "1" },
"sideB": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }]
},
"denomUnits": [{ "decimals": "6", "symbol": "ATOM", "isDefaultDisplay": true, "metadata": { "uri": "ipfs://METADATA_WRAPPER_uatom_UNIT", "customData": "" } }],
"metadata": { "uri": "ipfs://METADATA_WRAPPER_uatom", "customData": "" },
"allowOverrideWithAnyValidToken": false
}]
}Optional: Unbacking Withdraw Limits
Add approvalAmounts to the unbacking approval to enforce daily or total withdraw limits.
Critical: approvalAmounts are in base units, not display units. The value you write is in the alias denom's base units (e.g. uusdc, uatom), not the display unit (USDC, ATOM). You must convert by multiplying the user's stated amount by 10^decimals.
For a 6-decimal denom (USDC, ATOM, vUSDC, etc.):
| User says (display) | Write as (base units) |
|---|---|
| 1 USDC / day | "1000000" |
| 100 USDC / day | "100000000" |
| 1000 USDC / day | "1000000000" (nine zeros, not six) |
| 10000 USDC / day | "10000000000" |
Formula: base_units = display_amount * 10^decimals. For 6 decimals, that's display_amount followed by six zeros. Always sanity-check: 1 display unit = 10^decimals base units, so "1000000" (six zeros) = 1 display unit, not 1000. A common mistake is to compute "1000 × 10^6 = 1000000" (wrong; it's 1,000,000,000).
{
"approvalCriteria": {
"mustPrioritize": true,
"allowBackedMinting": true,
"overridesFromOutgoingApprovals": false,
"approvalAmounts": {
"overallApprovalAmount": "0",
"perFromAddressApprovalAmount": "1000000000",
"perToAddressApprovalAmount": "0",
"perInitiatedByAddressApprovalAmount": "0",
"amountTrackerId": "daily-withdraw-limit",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "86400000" }
}
}
}The example above encodes a 1000 USDC/day limit (1000 × 10^6 = 1,000,000,000 base units).
- Daily limit: Use perFromAddressApprovalAmount with resetTimeIntervals.intervalLength: "86400000" (24 hours in ms)
- Total limit: Use overallApprovalAmount with intervalLength: "0" (no reset)
- amountTrackerId must be unique per approval
- When refining: if the user reports "rate is X currently" and the on-chain value is already in base units, compare apples to apples, convert their stated X to base units via the formula above before concluding whether the current value matches.
Optional: 2FA on Unbacking
Require ownership of a 2FA token to withdraw. Add mustOwnTokens to the unbacking approval:
{
"approvalCriteria": {
"mustPrioritize": true,
"allowBackedMinting": true,
"overridesFromOutgoingApprovals": false,
"mustOwnTokens": [{
"collectionId": "74",
"amountRange": { "start": "1", "end": "18446744073709551615" },
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"overrideWithCurrentTime": true,
"mustSatisfyForAllAssets": false,
"ownershipCheckParty": "initiator"
}]
}
}- collectionId: the 2FA collection ID
- overrideWithCurrentTime: true ensures the check uses the current time (important for expiring 2FA tokens)
- ownershipCheckParty: "initiator" checks the person initiating the withdrawal
Metadata Guidance
- Collection, token, alias path, and denomUnit metadata proto fields are all shaped as
{ uri, customData }. Use placeholder URIs (ipfs://METADATA_COLLECTION,ipfs://METADATA_TOKEN_<id>,ipfs://METADATA_ALIAS_<denom>, etc.) and register the real names, descriptions, and images in themetadataPlaceholderssidecar keyed by those URIs. The auto-apply flow uploads the off-chain JSON and substitutes the placeholder URIs with real uploaded URIs after deploy. - Do not use lazy placeholder names like "Backing Approval", write real user-facing descriptions inside the metadataPlaceholders entries explaining what each approval / collection / token does.
Key Rules Summary
- No fromListId: "Mint" approvals: Tokens are created via IBC backing, not traditional minting
- Use allowBackedMinting: true in both backing and unbacking approvals
- Use mustPrioritize: true (required for IBC backed operations)
- overridesFromOutgoingApprovals: leave unset or false on both backing and unbacking approvals. The backing address is protocol-controlled but we still manage its approvals at the protocol level, treat it like any regular user whose outgoing approvals are externally managed. Don't set
overridesFromOutgoingApprovals: trueas a "best practice", adding overrides where they aren't needed is actively worse than omitting them. - Unbacking fromListId: Use
!Mint:backingAddresssyntax, excludes both Mint and backing address so only regular holders can send tokens back - Must create backing + unbacking approvals. Transferable approval is common but optional (omit for vaults/escrows)
- Must configure alias path with matching decimals
- The backing address is deterministic, use the one from generate_backing_address tool
Common Mistakes
- Don't use fromListId: "Mint", smart tokens are created via IBC backing, not traditional minting. Use the backing address as fromListId instead.
- Don't forget mustPrioritize: true on backing and unbacking approvals, without it, the chain cannot match the approval and the transfer fails.
- Don't assume all three approvals are required, the transferable approval is optional. Omit it for vaults or escrows where tokens should not move between users.
- Don't use "All" as fromListId or toListId for backing/unbacking operations, use the exact deterministic backing address from generate_backing_address.
- Don't use number types for amounts or IDs, all values must be strings ("1" not 1, "18446744073709551615" not Number.MAX_SAFE_INTEGER).
- Don't forget to configure an alias path, smart tokens will not function without one, and the alias decimals must match the IBC denom decimals.