Skip to main content

Overview

Cowboy has protocol-native fungible tokens (CIP-20): token state lives in the chain’s state tree and is manipulated through system instructions, not per-token smart contracts. You interact with tokens two ways:
  • From the CLIcowboy token <subcommand> for scripting and operations
  • From actor coderuntime.token_* host functions, so actors can create and move tokens as part of their logic
Amounts are integers in the token’s base unit (u128); decimals is display metadata only.

From the CLI

The full command surface is specified in cowboy token. A typical lifecycle:
# Create (you become owner, mint and freeze authority)
cowboy token create --name "Demo Token" --symbol DEMO \
  --decimals 9 --initial-supply 1000000 --max-supply 2000000

# Inspect and query (read-only — no key required)
cowboy token info --token-id 0x<TOKEN_ID>
cowboy token balance --token-id 0x<TOKEN_ID> --address 0x<ADDR>
cowboy token list

# Move
cowboy token transfer --token-id 0x<TOKEN_ID> --to 0x<ADDR> --amount 100
cowboy token approve --token-id 0x<TOKEN_ID> --spender 0x<ADDR> --amount 50

# Supply management (authorities only)
cowboy token mint --token-id 0x<TOKEN_ID> --to 0x<ADDR> --amount 500
cowboy token burn --token-id 0x<TOKEN_ID> --amount 100

# Compliance controls (freeze authority only)
cowboy token freeze --token-id 0x<TOKEN_ID> --account 0x<ADDR>
cowboy token unfreeze --token-id 0x<TOKEN_ID> --account 0x<ADDR>

From actor code

Actors get the same operations through cowboy_sdk.runtime. The caller is the actor itself — the actor’s address is the token owner / sender of transfers:
from cowboy_sdk import actor, public, runtime


@actor
class Treasury:
    @public
    def create_token(self, payload):
        token_id = runtime.token_create(
            b"Demo Token",      # name
            b"DEMO",            # symbol
            6,                  # decimals
            1_000_000_000_000,  # initial_supply (base units, credited to this actor)
            None,               # max_supply (None = uncapped)
            None,               # transfer_hook (None = no hook)
            None,               # metadata_uri
        )
        self.storage["token_id"] = token_id.hex()
        return b"ok"

    @public
    def pay(self, payload):
        token_id = bytes.fromhex(self.storage["token_id"])
        to = bytes.fromhex("...")        # 20-byte recipient address
        runtime.token_transfer(token_id, to, 1_000_000)
        return b"ok"
The query/move surface mirrors the CLI:
FunctionPurpose
runtime.token_create(name, symbol, decimals, initial_supply, max_supply, transfer_hook, metadata_uri)Create a token; returns the 32-byte token_id
runtime.token_balance_of(token_id, owner)Balance query
runtime.token_total_supply(token_id)Supply query
runtime.token_transfer(token_id, to, amount)Transfer from the calling actor
runtime.token_approve(token_id, spender, amount)Set an allowance
runtime.token_allowance(token_id, owner, spender)Allowance query
runtime.token_transfer_from(token_id, from_addr, to, amount)Spend an allowance
runtime.token_mint(token_id, to, amount)Mint (requires mint authority)
runtime.token_burn(token_id, amount)Burn from the caller’s balance
Each operation has a fixed gas cost (e.g. a transfer is 1,000 Cycles + 64 Cells) — see CIP-20 for the schedule.

Transfer hooks

A token can name a hook actor at creation (transfer_hook) to enforce custom transfer policy — blocklists, pausing, compliance logic. The hook implements two handlers:
  • can_transfer(payload) — called before balances move. Runs read-only (state writes are not committed). The check is fail-closed: the transfer proceeds only if the hook returns "true", "1", or JSON true; any other output (including b"ok" or an error) rejects it, reverting with TokenHookRejected.
  • on_transfer(payload) — called after balances move, and may update the hook actor’s own state. Best-effort: a failure here is logged but does not revert the transfer.
Both receive a JSON payload:
{
  "token_id": "<hex>",
  "from": "<hex 20-byte address>",
  "to": "<hex 20-byte address>",
  "amount": "<decimal string>"
}
import json
from cowboy_sdk import actor, public


@actor
class ComplianceHook:
    @public
    def can_transfer(self, payload):
        req = json.loads(payload)
        blocked = self.storage.get("blocklist", [])
        if req["from"] in blocked or req["to"] in blocked:
            return b"false"
        return b"true"

    @public
    def on_transfer(self, payload):
        # notification only — a failure here never reverts the transfer
        return b"ok"
Hooks run under hard sub-limits (token_hook_max_cycles / token_hook_max_cells, 50,000 each) regardless of the transaction’s gas budget — keep them small. Hook reentrancy into the same token is blocked by the runtime.

Worked example

examples/01-tokens/ in the repo pairs a CLI walkthrough (demo.sh) with an actor that creates and transfers a token programmatically (token_actor_example.py) — a good starting template for both styles.

Further reading