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 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
| Method | Description |
|---|---|
asset.Define(symbol, decimals, manager, creator, max_supply, enable_events) | Define asset properties (call in deploy) |
Transfers & Ownership
| Method | Description |
|---|---|
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
| Method | Returns | Description |
|---|---|---|
asset.BalanceOf(token_id, address) | U256 | Balance of address |
asset.Symbol() | String | Asset symbol |
asset.Creator() | Identifier | Creator address |
asset.Manager() | Identifier | Manager address |
asset.Decimals() | U64 | Decimal places |
asset.MaxSupply() | U256 | Maximum supply |
asset.CirculatingSupply() | U256 | Current supply |
asset.EnableEvents() | Bool | Whether events enabled |
Metadata
| Method | Description |
|---|---|
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 methods | Required qualifier |
|---|---|
Transfer, TransferFrom, Mint, MintWithMetadata, Burn, Approve, Revoke, Lockup, Release | dynamic |
SetStaticMetadata, SetDynamicMetadata, SetStaticTokenMetadata, SetDynamicTokenMetadata | dynamic |
Symbol, Decimals, BalanceOf, Creator, Manager, MaxSupply, CirculatingSupply, EnableEvents | static |
GetStaticMetadata, GetDynamicMetadata, GetStaticTokenMetadata, GetDynamicTokenMetadata | static |
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
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.
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)
- Only asset logic can access the
assetglobal object - Regular logic must use interfaces to interact with assets
- Asset methods dictate the endpoint's state qualifier — writes are
dynamic, reads arestatic - This separation ensures security and programmability