Logics
A developer guide to writing, compiling, deploying, and interacting with logics on MOI using CoCo and the js-moi-sdk.
Prerequisites: This guide assumes familiarity with MOI's execution architecture — including interactions, the ISM, and the IEE/LEE runtime. If these concepts are new, start with the Execution documentation first.
1. What is a Logic?
On MOI, a Logic is not a smart contract in the traditional sense. It is a deterministic rule-set that defines how state transitions occur, but it does not hold the state.
MOI decouples code from state:
- The Logic — Pure code. It defines the rules (e.g., "To transfer token X, signature Y is required"). It is stateless and non-custodial.
- The Participant — The user's Lattice holds the actual assets and data.
When a logic is invoked, the protocol brings the logic to the participant's data, executes it, and returns the result. The logic never takes custody of the participant's assets or state.
1.1 Why Does This Matter?
This decoupling fundamentally changes how you design, secure, and scale applications:
- Elimination of honey pots — You no longer ask users to deposit funds into your contract address. Your logic simply validates the movement of funds within the user's own Lattice. If your logic has a bug, only the specific transaction fails — the total pool of user assets remains decentralized and out of reach.
- Protocol-level safety — Entire classes of exploits (like re-entrancy draining a contract) are physically impossible because the logic never takes custody of the assets it manages.
- Seamless upgradeability — You can upgrade or swap your logic without needing to migrate assets. Since the assets never lived inside the code, a new logic version can simply be invited to interact with the existing user data.
- Hyper-parallel performance — By designing your application to use actor state (user-owned data), your application scales linearly with your user base rather than fighting for a single global main thread.
1.2 When Should You Use Logics?
Logics are the right tool when you need:
Custom business rules — Application-specific validation, calculations, or state transitions that go beyond protocol-native operations. For example, in a decentralized ride-share app, the logic wouldn't hold the passenger's payment. Instead, it would contain the formula to calculate the fare based on GPS data, while the payment moves directly peer-to-peer from passenger to driver once the logic validates the calculation.
Domain-specific workflows — Complex multi-step processes like auctions, escrows, DAOs, or games. In a MOI escrow, the logic doesn't hold the funds like an Ethereum contract would. Instead, the logic acts as a key that requires specific conditions (like buyer and seller signatures) to unlock the funds. The funds remain in the participants' own accounts — secured by the logic's rules but never held inside the logic itself.
2. State Management
Choosing the right state model is the single most important architectural decision you will make when building on MOI.
In Ethereum, every variable in a smart contract is effectively a global variable. If you build a game, every player's score lives in the same database table. This forces the network to lock the entire table for every single update, creating a massive bottleneck.
MOI solves this by partitioning state into two distinct buckets: Logic State and Actor State.
2.1 The Dual-State Architecture
Logic State is shared data stored on the logic itself. It functions like a global variable — visible to everyone, but only one person can write to it at a time. The network must enforce strict ordering on updates.
Actor State is isolated data stored on the user's Lattice. It functions like a local variable unique to each participant. Even though your logic defines the structure, the data lives with the user, allowing everyone to update their own state simultaneously.
| Feature | Logic State | Actor State |
|---|---|---|
| Concept | Shared memory | Local memory |
| Ownership | Owned by the logic | Owned by the participant |
| Concurrency | Sequential (queue-based) | Hyper-parallel (instant) |
| Best for | Global counters, total supply, admin configs | User balances, game inventories, voting history |
3. Manifest and Artifact
When you compile a CoCo logic, the output is a manifest — a single file that contains everything the runtime needs to execute that logic.
3.1 What the Manifest Contains
When you compile the Counter logic, the output is a manifest file (counter.json).
Expand the sample manifest
{
"syntax": 1,
"engine": {
"kind": "PISA",
"flags": [],
"version": "0.5.0"
},
"kind": "logic",
"elements": [
{
"ptr": 0,
"kind": "state",
"data": {
"mode": "logic",
"fields": [
{ "slot": 0, "label": "count", "type": "u64" }
]
}
},
{
"ptr": 1,
"deps": [0],
"kind": "callable",
"data": {
"name": "Seed",
"mode": "dynamic",
"kind": "deploy",
"accepts": [
{ "slot": 0, "label": "initial_value", "type": "u64" }
],
"returns": [],
"executes": { "bin": [...] },
"catches": []
}
},
{
"ptr": 2,
"deps": [0],
"kind": "callable",
"data": {
"name": "Increment",
"mode": "dynamic",
"kind": "invoke",
"accepts": [],
"returns": [],
"executes": { "bin": [...] },
"catches": []
}
},
{
"ptr": 3,
"deps": [0],
"kind": "callable",
"data": {
"name": "GetCount",
"mode": "static",
"kind": "invoke",
"accepts": [],
"returns": [
{ "slot": 0, "label": "current_count", "type": "u64" }
],
"executes": { "bin": [...] },
"catches": []
}
}
]
}
Here's what each part of the manifest does:
Engine metadata — declares the target VM so the LEE knows where to route execution.
"engine": { "kind": "PISA", "flags": [], "version": "0.5.0" }
Logic kind — indicates whether this is a regular logic or an asset logic.
"kind": "logic"
State definitions — defines the persistent state fields, their types, and slot positions. This is what persistentState.get() resolves against from the SDK.
{
"ptr": 0,
"kind": "state",
"data": {
"mode": "logic",
"fields": [
{ "slot": 0, "label": "count", "type": "u64" }
]
}
}
Endpoint signatures — each callable entry defines an endpoint's name, mode (static/dynamic), kind (deploy/invoke), and its parameter and return types. This is the schema the SDK reads to POLO-encode call data and decode results.
{
"name": "Seed",
"mode": "dynamic",
"kind": "deploy",
"accepts": [
{ "slot": 0, "label": "initial_value", "type": "u64" }
],
"returns": []
}
Bytecode — the executes.bin field contains PISA bytecode. Opaque to developers, executed by the VM.
"executes": { "bin": [...] }
Dependencies — the deps field lists which state elements an endpoint depends on, enabling the runtime to resolve the correct context objects.
"deps": [0]
Logic kind — the manifest also declares the logic's interaction compatibility:
- LogicKind — Generic state machine for DAOs, games, custom apps
- AssetKind — Protocol-native asset standard with native minting/burning
The manifest schema is what the SDK reads to serialize call data when invoking endpoints, and what clients use to generate storage keys when reading state directly from the database.
3.2 Artifact
An artifact is a packaged wrapper around compiled logic. It bundles the logic's manifest together with execution metadata such as the target callsite and encoded calldata. This allows tooling to load a single file containing both the compiled program and the information required to invoke it.
3.3 Logic Object
When a manifest is deployed to the network, the protocol instantiates a Logic Object — a protocol-level construct that holds the logic's identity and runtime configuration:
- A unique Logic ID used to address the logic in all future interactions
- The engine kind (e.g., PISA) indicating which VM backend executes the bytecode
- A hash of the manifest for interface integrity verification
- A sealed flag — a sealed logic is permanently immutable; unsealed logics can be updated in place
- The artifact — the compiled bytecode instructions executed by the VM
Once deployed, the Logic ID is publicly addressable on the network. Multiple participants can invoke the same Logic Object independently.
4. Storage Layout
Under the hood, all state in MOI is stored in a flat key-value database. The storage layout defines how CoCo's high-level types (fields, arrays, maps, classes) are converted into database keys.
4.1 Primitives
For simple types, the slot number is the key. A Bool at slot 0 and a String at slot 1:
slot 0 -> value of Bool
slot 1 -> value of String
4.2 Arrays
Arrays are split into individual entries. The length is stored at the slot key, and each element gets its own key at hash(slot) + index:
state logic:
scores []U64 // slot 0, value: [10, 20, 30]
key 0 -> 3 (length)
key hash(0) + 0 -> 10 (element 0)
key hash(0) + 1 -> 20 (element 1)
key hash(0) + 2 -> 30 (element 2)
The hash spreads element keys across the database so they don't collide with other slots.
4.3 Maps
Maps work similarly, but the map key is hashed and concatenated with the slot. The length is stored at the slot key, and each value is stored at hash(slot . hash(key)):
state logic:
balances Map[String]U64 // slot 0, value: {"alice": 100, "bob": 200}
key 0 -> 2 (length)
key hash(0 . hash("alice")) -> 100 (value for "alice")
key hash(0 . hash("bob")) -> 200 (value for "bob")
The . here means byte concatenation. Map keys are hashed because they can be any size, and database keys need to be consistent.
This is also why you cannot iterate over maps in CoCo. There is no master list of keys — each entry is stored at a separate hashed location. Unless you already know the key, you can't find the entry.
4.4 Classes
Each field in a class gets its own key at hash(slot) + fieldIndex:
class Person:
field name String
field age U64
// Person at slot 0, value: { name: "alice", age: 25 }
key hash(0) + 0 -> "alice" (field 0: name)
key hash(0) + 1 -> 25 (field 1: age)
4.5 Nested Types
For nested types (a map of arrays, an array of classes, etc.), the same rules apply recursively. The inner type uses the outer type's key formula as its base.
For example, data: Map[String][]U64 at slot 0 with value {"foo": [10, 20]}:
key 0 -> 1 (length of map)
key hash(0 . hash("foo")) -> 2 (length of array at "foo")
key hash(hash(0 . hash("foo"))) + 0 -> 10 (foo[0])
key hash(hash(0 . hash("foo"))) + 1 -> 20 (foo[1])
Each nesting layer wraps another hash around the previous formula.
4.6 Why This Matters
This storage layout is conceptually similar to how Ethereum's EVM handles storage — Solidity uses keccak256(key, slot) for mappings and sequential slots for primitives. But there's a key difference in how developers access it.
In Ethereum, to read storage directly (via getStorageAt), you need to reverse-engineer the storage layout from the Solidity compiler output. The ABI doesn't include storage slot information — it only describes function signatures. So figuring out that balances[alice] lives at keccak256(alice, 0) requires understanding Solidity's internal layout rules, which are compiler-specific and undocumented at the protocol level.
In MOI, the manifest includes the complete storage schema — field names, types, slot positions, and nesting structure. You never reverse-engineer anything. The SDK reads the manifest and generates the correct database keys automatically. This is why persistentState.get() works with a clean builder API rather than raw slot calculations:
// SDK reads manifest → knows "count" is u64 at slot 0 → key is just 0
const count = await logic.persistentState.get(access => access.entity("count"));
// SDK reads manifest → knows "balances" is Map at slot 0 → computes hash(0 . hash(address))
const balance = await logic.persistentState.get(access =>
access.entity("balances").property(hexToBytes(address))
);
The .entity(), .property(), .at(), and .field() chain in the SDK maps directly to the nesting rules described above — each call adds another layer of key computation.
5. Anatomy of a CoCo Logic
5.1 Basic Structure
Every CoCo logic begins with a module declaration (coco ModuleName) that defines the logic's identity. This name becomes the namespace for all state access throughout the code — similar to this in Java or self in Python.
A complete CoCo logic consists of:
- Module declaration — The identity of the logic
- State blocks — Data storage definitions (
state logic:andstate actor:) - Endpoints — Externally callable entry points
- Functions — Internal helper routines (optional)
- Classes — Custom data structures (optional)
- Interfaces — Connections to external logics (optional)
Logic State Example:
coco Flipper
state logic:
values Map[Identifier]Bool
endpoint deploy Init():
mutate values <- Flipper.Logic.values:
values[Sender] = true
endpoint dynamic Flip():
mutate values <- Flipper.Logic.values:
values[Sender] = !values[Sender]
Actor State Example:
coco ContextFlipper
state actor:
value Bool
endpoint enlist Init():
mutate true -> ContextFlipper.Sender.value
endpoint dynamic Flip():
mutate value <- ContextFlipper.Sender.value:
value = !value
5.2 Endpoint Qualifiers
Endpoints support several qualifiers that define their behavior:
| Qualifier | Purpose |
|---|---|
deploy | One-time initialization when logic is first deployed |
enlist | Per-actor initialization on first user interaction |
dynamic | Write access to state (required for mutate) |
asset | Asset engine access for minting, burning, transferring |
| (none) | Read-only / static (default) |
All arguments and return values must be named — CoCo forbids anonymous parameters for clarity and auditability.
6. Interfaces
Interfaces are CoCo's composition primitive. They let one logic read state and invoke endpoints on another deployed logic within the same interaction.
An interface is a partial description — you declare only the fields and endpoints you need from the external logic, not its entire structure. It has four optional sections: state logic, state actor, endpoint, and asset. You can directly observe (read) an external logic's state through an interface, but you cannot mutate it — to write, you must call an endpoint that performs the mutation on your behalf.
Interfaces are generic until bound to a specific Logic ID at runtime, so the same definition can be reused with any logic that matches its shape — a payment processor can work with any asset that exposes a Transfer endpoint, without being hardcoded to one.
6.1 Caller Identity
In cross-logic calls, Sender always refers to the original actor who initiated the interaction. To identify which logic made the immediate call, CoCo provides Invocation.Caller(). You can also specify an alternative sender when binding, enabling multi-participant flows like atomic swaps where two users sign the same interaction.
6.2 Example
interface TokenInterface:
state logic:
TotalSupply U256
endpoint:
dynamic Transfer(to Identifier, amount U256)
endpoint dynamic SendTokens(token_logic_id Identifier, to Identifier, amount U256):
memory token = TokenInterface(token_logic_id)
observe supply <- token.Logic.TotalSupply
token.Transfer(to: to, amount: amount)
7. Events
Events let a logic broadcast structured data to off-chain systems (indexers, UIs, analytics) without requiring state queries. They are captured in the Interaction Receipt inside a Tesseract.
7.1 Event Structure
An event is declared with the event keyword and contains two kinds of entries:
- Topics (up to 4) — indexed for efficient filtering
- Fields (up to 256) — non-indexed data payload
Only primitive types are allowed in both.
7.2 Emission Patterns
Events can be emitted in two ways:
- Directly — by constructing the event inline with
emit MyEvent{...} - Via a class — that implements the
__event__()method, letting youemita class instance and have CoCo extract the event automatically
7.3 Event Context
Every emitted event is logged to a context. By default, emit Event{...} logs to the logic context (global to the application). Appending -> ActorId logs it to a specific actor's context instead — useful when the event is relevant to a particular participant, like a balance change notification.
7.4 Example
event TransferEvent:
topic operation String
field from Identifier
field amount U256
endpoint invoke RecordTransfer(amount U256):
emit TransferEvent{
operation: "Transfer",
from: Sender,
amount: amount
} -> Sender
7.5 Reading Events from JavaScript
After an endpoint emits events, they are captured in the interaction receipt. From JavaScript, you decode the raw event data using the manifest schema.
import { ManifestCoder, getLogicDriver } from "js-moi-sdk";
import manifest from "./counter.json" with { type: "json" };
const logic = await getLogicDriver(LOGIC_ID, wallet);
// 1. Execute an endpoint that emits events
const ix = await logic.routines.RecordTransfer(1000);
const receipt = await ix.wait();
// 2. Decode the event from the receipt
const manifestCoder = new ManifestCoder(manifest);
const eventData = manifestCoder.decodeEventOutput("TransferEvent", receipt.logs[0].data);
console.log(eventData);
// { operation: "Transfer", from: "0x...", amount: 1000n }
The event data in the receipt is raw POLO-encoded bytes. decodeEventOutput reads the event definition from the manifest schema and deserializes the data back into a structured object. This is the same manifest-as-shared-contract pattern used for call data encoding and state retrieval.
8. Logic Lifecycle
This section walks through the full lifecycle of a logic module using a Counter example.
8.1 Authoring
Write your logic in CoCo, defining endpoints and state declarations.
coco Counter
state logic:
count U64
endpoint deploy Seed(initial_value U64):
mutate initial_value -> Counter.Logic.count
endpoint dynamic Increment():
mutate current_count <- Counter.Logic.count:
current_count = current_count + 1
endpoint GetCount() -> (current_count U64):
observe current_count <- Counter.Logic.count
8.2 Compilation
Compile the module to produce a manifest.
coco compile
The output is a manifest file (e.g., counter.json) containing the schema (endpoint signatures, storage slot IDs, type definitions), PISA bytecode, and VM metadata.
8.3 Deployment
Submit the manifest to the network through a deployment interaction. The protocol instantiates a Logic Object and, if a deploy endpoint is defined, executes it to initialize the logic's persistent state.
import { Wallet, JsonRpcProvider, LogicFactory } from "js-moi-sdk";
import manifest from "./counter.json" with { type: "json" };
const MNEMONIC = process.env.MOI_MNEMONIC;
async function deploy() {
const provider = new JsonRpcProvider("https://dev.voyage-rpc.moi.technology/devnet");
const wallet = await Wallet.fromMnemonic(MNEMONIC, "m/44'/6174'/7020'/0/0");
wallet.connect(provider);
const factory = new LogicFactory(manifest, wallet);
// "Seed" is the deploy endpoint name; 0 is the initial_value argument
const deployIx = await factory.deploy("Seed", 0);
const result = await deployIx.result();
console.log("Logic ID:", result.logic_id);
}
deploy().catch(console.error);
Deployment happens once per logic instance. Once deployed, the Logic ID is publicly addressable on the network.
8.4 Invocation
Invoke the logic through LOGIC_INVOKE interactions. The SDK reads the manifest schema, POLO-encodes the inputs, and submits the interaction.
import { Wallet, JsonRpcProvider, getLogicDriver } from "js-moi-sdk";
import manifest from "./counter.json" with { type: "json" };
const MNEMONIC = process.env.MOI_MNEMONIC;
const LOGIC_ID = "0x...";
async function interact() {
const provider = new JsonRpcProvider("https://dev.voyage-rpc.moi.technology/devnet");
const wallet = await Wallet.fromMnemonic(MNEMONIC, "m/44'/6174'/7020'/0/0");
wallet.connect(provider);
const driver = await getLogicDriver(LOGIC_ID, manifest, wallet);
// Execute logic
const ix = await driver.routines.Increment();
await ix.wait();
// Observe results
const count = await driver.routines.GetCount();
console.log("Current count:", count);
}
interact().catch(console.error);
The runtime receives the interaction, routes it to the LEE, loads the manifest, resolves state, executes bytecode through PISA, and integrates results with protocol engines.
8.5 State Transition and Finalization
After execution, state transitions are applied and the system generates a Tesseract — a cryptographic proof block containing the interaction receipt, state commitments, and a new context hash.
┌─────────────────────────────────────────────────────────────┐
│ INTERACTION FLOW │
├─────────────────────────────────────────────────────────────┤
│ │
│ User Wallet │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ LOGIC_INVOKE │ Typed interaction │
│ │ target: Counter │ │
│ │ method: Incr() │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ ISM │ Load participant context │
│ │ │ Load logic state │
│ │ f(state) → │ Execute: count = count + 1 │
│ │ state' │ Validate & commit │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ TESSERACT │ Cryptographic proof │
│ │ │ Height: N+1 │
│ │ ContextHash │ New state root │
│ │ Receipt │ Events captured │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
The receipt records interaction status, fuel used, participants involved, and per-interaction results. Events emitted during execution are captured in the receipt.
Each Tesseract cryptographically references the previous context hash, creating an immutable, linked history of state transitions for each participant.
To explore CoCo in depth with interactive examples, visit cocolang.dev
Glossary
| Term | Definition |
|---|---|
| Logic | A deterministic rule-set deployed on MOI that validates state transitions without holding assets |
| Logic State | Persistent data stored on the logic itself, requiring sequential access |
| Actor State | Persistent data stored on each participant's context, enabling hyper-parallel execution |
| Manifest | The canonical schema of a deployed logic — lists endpoints, types, state dependencies, and permissions |
| Artifact | The compiled bytecode of a CoCo logic, executed by the PISA engine |
| Logic Object | The protocol-level construct instantiated on deployment, holding the logic's identity and runtime configuration |
| CoCo | The programming language for writing MOI logics |
| PISA | The execution engine that runs compiled CoCo bytecode |
| POLO | Prefix Ordered Lookup Offsets — the binary serialization format used for data exchange in MOI |
| Endpoint | An externally callable entry point in a CoCo logic |
| Tesseract | A cryptographic proof block containing interaction receipts and state commitments |
| Lattice | A participant's individual state tree, isolated from other participants for parallel execution |
| LogicFactory | SDK helper that packages a manifest and submits a deployment interaction to the network |
| Interface | A partial description of an external logic used for cross-logic composition and invocation |