Skip to main content

Assets

Native programmable assets are a core concept in MOI. Asset rules (minting, burning, transfers, approvals) are enforced at the protocol level through asset logic written in Coco.

Asset flow: User → Logic → Asset Logic → Asset Engine

Asset Logic

Declare an asset logic with the asset keyword:

coco asset MyToken

Asset logic has privileged access to the asset engine via the asset global object.

Asset Engine Methods

Lifecycle

MethodDescription
asset.Define(symbol, decimals, manager, creator, max_supply, enable_events)Define asset properties (call in deploy)

Transfers & Ownership

MethodDescription
asset.Transfer(token_id, beneficiary, amount)Transfer from Sender to beneficiary
asset.TransferFrom(token_id, benefactor, beneficiary, amount)Transfer from benefactor (requires approval)
asset.Mint(token_id, beneficiary, amount)Mint new tokens
asset.MintWithMetadata(token_id, beneficiary, amount, static_metadata)Mint with metadata
asset.Burn(token_id, amount)Burn tokens from Sender
asset.Lockup(token_id, beneficiary, amount)Lock tokens
asset.Release(token_id, benefactor, beneficiary, amount)Release locked tokens
asset.Approve(token_id, beneficiary, amount, expires_at)Approve spending
asset.Revoke(token_id, beneficiary)Revoke approval

Queries

MethodReturnsDescription
asset.BalanceOf(token_id, address)U256Balance of address
asset.Symbol()StringAsset symbol
asset.Creator()IdentifierCreator address
asset.Manager()IdentifierManager address
asset.Decimals()U64Decimal places
asset.MaxSupply()U256Maximum supply
asset.CirculatingSupply()U256Current supply
asset.EnableEvents()BoolWhether events enabled

Metadata

MethodDescription
asset.SetStaticMetadata(key, value)Set asset-level static metadata
asset.SetDynamicMetadata(key, value)Set asset-level dynamic metadata
asset.GetStaticMetadata(key)Read static metadata → Bytes
asset.GetDynamicMetadata(key)Read dynamic metadata → Bytes
asset.SetStaticTokenMetadata(token_id, key, value)Per-token static metadata
asset.SetDynamicTokenMetadata(token_id, key, value)Per-token dynamic metadata
asset.GetStaticTokenMetadata(token_id, key)Read per-token static → Bytes
asset.GetDynamicTokenMetadata(token_id, key)Read per-token dynamic → Bytes

State Qualifiers

Every asset engine method carries a required state qualifier, and the compiler enforces an exact match: the endpoint or function calling it must declare that qualifier — no lower and no higher.

Asset methodsRequired qualifier
Transfer, TransferFrom, Mint, MintWithMetadata, Burn, Approve, Revoke, Lockup, Releasedynamic
SetStaticMetadata, SetDynamicMetadata, SetStaticTokenMetadata, SetDynamicTokenMetadatadynamic
Symbol, Decimals, BalanceOf, Creator, Manager, MaxSupply, CirculatingSupply, EnableEventsstatic
GetStaticMetadata, GetDynamicMetadata, GetStaticTokenMetadata, GetDynamicTokenMetadatastatic

The rule is short: every asset write is dynamic, every asset read is static.

endpoint dynamic Transfer(beneficiary Identifier, amount U256):
asset.Transfer(token_id: 0, beneficiary, amount)

endpoint static BalanceOf(address Identifier) -> (balance U256):
balance = asset.BalanceOf(token_id: 0, address)

The requirement is the maximum over everything the body does — its own observe/mutate, the asset methods it calls, the functions it calls, and any cross-logic interface calls. An endpoint that reads a balance and also mutates state is dynamic. asset.Define() needs no qualifier, and deploy/enlist endpoints are exempt because they are dynamic by definition.

A mismatch is a compile error:

invalid function: function 'Transfer' is declared as 'pure', but it requires state qualifier 'dynamic'
invalid function: function 'Symbol' is declared as 'pure', but it requires state qualifier 'static'

Example

my_token.coco
coco asset MyToken

event TransferEvent:
topic from Identifier
topic to Identifier
field amount U256

endpoint dynamic Transfer(to Identifier, amount U256):
if asset.EnableEvents():
emit TransferEvent{from: Sender, to: to, amount: amount}

asset.Transfer(token_id: 0, beneficiary: to, amount: amount)

endpoint dynamic Mint(to Identifier, amount U256):
if Sender != asset.Manager():
throw "Unauthorized"

asset.Mint(token_id: 0, beneficiary: to, amount: amount)

Calling Asset Logic

Regular logic cannot access the asset engine directly. It must call asset logic through an interface.

payment.coco
coco Payment

interface Token:
asset:
Transfer(to Identifier, amount U256)

endpoint dynamic Pay(token_id Identifier, recipient Identifier, amount U256):
memory token = Token(token_id)
token.Transfer(to: recipient, amount: amount)

A cross-logic asset call always counts as a mutation of the asset's state, so any endpoint that calls one through an interface — including a read like Symbol() or BalanceOf() — must be dynamic.

Creating Assets

Assets are created via MOI interactions, not directly in Coco. In Cocolab:

compile MyToken from manifest(mytoken.yaml)
create MyToken(symbol: "MTK", decimals: 18, manager: user1, max_supply: 1000000)
Key Points
  • Only asset logic can access the asset global object
  • Regular logic must use interfaces to interact with assets
  • Asset methods dictate the endpoint's state qualifier — writes are dynamic, reads are static
  • This separation ensures security and programmability