Assets
Native programmable assets are a core concept in MOI. While MOI achieves high performance via parallelism, asset logic enables developers to define safe, native asset behavior at the protocol level. An asset's rules—minting, burning, transfers, approvals, lockups, metadata—are enforced by the MOI asset engine and can only be accessed by an asset logic (written in Coco), attached to the asset, which defines the behaviour of the asset. This design gives you protocol-level safety (no userland balance bugs), flexible asset rules in asset logic and high performance through MOI's parallel execution.
An asset is created by interaction on MOI, Coco can't directly create assets. When asset is created, its properties are defined (symbol, decimals, manager, limits, events), but an essential part of asset creation is also deployment of asset logic that can be written in Coco. Regular logics namely cannot talk to the asset engine directly; they must call the asset logic's endpoints via an interface. This clean separation lets you expose a minimal, reviewed API for other logics while retaining privileged operations (e.g., mint/burn) under controlled endpoints.
Asset logic
Declare an asset logic by adding the asset keyword after the coco module declaration. Asset logics have access to the asset engine,
which provides the methods below to manage balances, allowances, and metadata.
| Method | Args | Returns | Description |
|---|---|---|---|
asset.Transfer | token_id U64, beneficiary Identifier, amount U256 | – | Transfer an approved amount to beneficiary. |
asset.TransferFrom | token_id U64, benefactor Identifier, beneficiary Identifier, amount U256 | – | Transfer an approved amount from benefactor to beneficiary. Anyone may call if approved. |
asset.Mint | token_id U64, beneficiary Identifier, amount U256 | – | Mint amount and credit beneficiary. |
asset.Burn | token_id U64, beneficiary Identifier, amount U256 | – | Burn amount from beneficiary. |
asset.Approve | token_id U64, beneficiary Identifier, amount U256, expires_at U64 | – | Approve up to amount for transfer to beneficiary until expires_at. Does not move funds. |
asset.Revoke | token_id U64, beneficiary Identifier | – | Revoke any existing approval for beneficiary. |
asset.Lockup | token_id U64, beneficiary Identifier, amount U256 | – | Lock amount for beneficiary (deducts immediately; cannot be revoked). |
asset.Release | token_id U64, benefactor Identifier, beneficiary Identifier, amount U256 | – | Release amount previously locked by benefactor to beneficiary. Anyone may call. |
asset.Symbol | – | symbol String | Asset symbol. |
asset.Decimals | – | decimals U64 | Number of decimals. |
asset.MaxSupply | – | max_supply U256 | Maximum supply. |
asset.CirculatingSupply | – | circulating_supply U256 | Total minted minus total burned. |
asset.BalanceOf | token_id U64, address Identifier | balance U256 | Balance of token_id at address. |
asset.Creator | – | creator Identifier | Creator address. |
asset.Manager | – | manager Identifier | Manager address. |
asset.EnableEvents | – | enable_events Bool | Whether asset emits events. |
asset.SetMetadata | key String, value Bytes | – | Set asset metadata entry. |
asset.GetMetadata | key String | value Bytes | Get asset metadata entry. |
asset.SetTokenMetadata | token_id U64, key String, value Bytes | – | Set token-scoped metadata for sender. |
asset.GetTokenMetadata | token_id U64, key String | value Bytes | Get token-scoped metadata for sender. |
Asset is created by passing the asset logic with asset properties in the interaction to create assets. In Cocolab it's simulated by issuing a command like
compile MyAsset from manifest(myasset.yaml)
create MyAsset(symbol: "MYASSET", decimals: 2, manager: default_user, max_supply: 10000, enable_events: true)
Once the asset is created, other logics can use the logic using an interface like in the example regular_logic.coco.
coco asset MyAsset
event AssetEvent:
topic operation String
field operator Identifier
field benefactor Identifier
field beneficiary Identifier
field amount U256
field expires_at U64
endpoint MyTransfer(beneficiary Identifier, amount U256):
if asset.EnableEvents():
emit AssetEvent{operation: "Transfer", operator: Sender, benefactor: Sender,
beneficiary: beneficiary, amount: amount}
// here the asset method is called, always for token 0 in this example
asset.Transfer(token_id: 0, beneficiary, amount)
coco RegularLogic
interface SomeAsset:
asset:
MyTransfer(receiver Identifier, amount U256)
// endpoint requires "asset" qualifier
endpoint Transfer(assetId Identifier, receiver Identifier, amount U256):
// we bind the interface to the concrete assetId
memory assetIface = SomeAsset(assetId)
// this is a call to asset logic, regular logics can't access "asset" object as the asset logic can
assetIface.MyTransfer(beneficiary: receiver, amount: amount)
// sends an asset with well known identifier to some constant receiver
const MYASSET Identifier = 0xd3b83c890d6ef90185c894e65052e2f18aefed8a171152d301bd20b9ae5ab9ed
const RECEIVER Identifier = 0xcadffe5d6654f1a6d2cc766d7ddaf8485307b2ebb351551b1a57bf1fcec54be5
endpoint MyPrecious(amount U256):
memory assetIface = SomeAsset(MYASSET)
assetIface.MyTransfer(beneficiary: RECEIVER, amount: amount)
Understanding Asset and Regular Logic Interaction
The examples above demonstrate two distinct types of logic working together: asset logic (which has privileged access to the MOI Asset Engine) and regular logic (which must go through asset logic to interact with assets).
Part 1: Asset Logic (asset_logic.coco)
This code defines the rules of the asset. It sits between the blockchain's core engine and the outside world.
The Header and Events
coco asset MyAsset
event AssetEvent:
topic operation String
field operator Identifier
field benefactor Identifier
field beneficiary Identifier
field amount U256
field expires_at U64
coco asset MyAsset: Theassetkeyword is crucial. It gives this code permission to talk to the MOI Asset Engine (the protocol layer that actually creates money). Without this keyword, you cannot mint or burn.event AssetEvent: This defines a structure for logging data. Blockchains don't have "console logs"; they have events. This defines what the log will look like (who did it, how much, etc.).
The Wrapper Endpoint
endpoint MyTransfer(beneficiary Identifier, amount U256):
- The Gatekeeper: This is a public function. Anyone can call it.
- Customization: Unlike a standard "Send" button, this allows you to add custom code before the transfer happens (e.g., checking a blocklist, taking a tax, or emitting a specific event).
The Logic (Event Emission)
if asset.EnableEvents():
emit AssetEvent{operation: "Transfer", operator: Sender, benefactor: Sender,
beneficiary: beneficiary, amount: amount}
asset.EnableEvents(): This checks a setting in the Asset Engine.emit: This broadcasts the event defined earlier to the blockchain history.Sender: This is a keyword representing the user calling the function.
The Protocol Call (The Magic Line)
// here the asset method is called, always for token 0 in this example
asset.Transfer(token_id: 0, beneficiary, amount)
asset.: This is the Superglobal object. It is the direct line to the MOI Protocol.Transfer(...): This command tells the MOI engine: "Actually move the balances now."token_id: 0: In MOI, one logic can manage multiple sub-tokens (like a game logic managing Gold, Silver, and Gems). Here, it hardcodes ID 0 (the default token).
Part 2: Regular Logic (regular_logic.coco)
This code represents a completely separate application (like a generic payment processor) that wants to interact with the asset defined above.
The Interface
interface SomeAsset:
asset:
MyTransfer(receiver Identifier, amount U256)
- The Contract: Since
RegularLogicdoesn't ownMyAsset, it doesn't know what functions exist. This block tells the compiler: "I expect there to be an Asset out there that has a function calledMyTransfer." asset:: Specifies that we are looking for an asset endpoint.
The Dynamic Endpoint (Handling Any Asset)
// endpoint requires "asset" qualifier
endpoint Transfer(assetId Identifier, receiver Identifier, amount U256):
assetId: This argument allows this logic to work with any asset. You could pass it the ID for Bitcoin, Stablecoin, or GameToken.
Binding and Execution
// we bind the interface to the concrete assetId
memory assetIface = SomeAsset(assetId)
// this is a call to asset logic
assetIface.MyTransfer(beneficiary: receiver, amount: amount)
SomeAsset(assetId): This acts like a "Cast" or "Binding." It says: "Take the addressassetIdand treat it like theSomeAssetinterface we defined earlier."assetIface.MyTransfer: This executes the call.- Crucial Note: This does NOT call the MOI Engine directly. It calls
MyTransferin the Asset Logic (Part 1). The Asset Logic then calls the Engine.
Hardcoded Constants (Specific Asset)
const MYASSET Identifier = 0xd3b8...
const RECEIVER Identifier = 0xcad...
endpoint MyPrecious(amount U256):
memory assetIface = SomeAsset(MYASSET)
assetIface.MyTransfer(beneficiary: RECEIVER, amount: amount)
const: These are fixed values.MyPrecious: This endpoint is "hardwired." Unlike theTransferendpoint above (which accepts any asset ID), this one only works with the specific address defined inMYASSETand sends toRECEIVER. This is useful for things like "Buy this specific item" buttons.
Summary of the Flow
- User calls
RegularLogic.Transfer. - RegularLogic uses the Interface to call
MyAsset.MyTransfer. - MyAsset receives the call, checks its own rules, and emits an event.
- MyAsset uses the
assetkeyword to tell the MOI Engine to move the money.
This clean separation ensures that:
- Only asset logic can directly manipulate balances (security)
- Regular logic can work with any asset through interfaces (flexibility)
- Custom rules and events can be enforced before protocol-level operations (programmability)