Cudo Language Reference
Everything you need to write, compile, and deploy Cudo contracts on Ferros. If you're coming from Solidity, also see the migration guide.
Cudo is a smart contract language where the compiler proves your parallelism is safe, double-spend is a type error, and reentrancy doesn't exist in the grammar. Every contract declares what state it touches, what it requires, and how it contends — the compiler holds you to it.
Installation
Cudo requires a Rust toolchain. Install it, then build from source:
# clone and build $ git clone https://github.com/user/cudo && cd cudo $ cargo build --release # verify $ cudo --version cudo 0.1.0
The CLI gives you four commands: cudo check, cudo compile, cudo run, and cudo inspect.
Hello World
The simplest Cudo contract: a counter. It declares one piece of state, one invariant, and two operations.
contract Counter { version: 1 contention: global state { count: u64, } operation increment() mutates: self.count { self.count += 1; } view get() -> u64 { self.count } }
$ cudo check counter.cudo
✓ counter.cudo: 0 errors, 0 warnings
contention: global (1 thread)
Contracts
Every Cudo program is a contract. A contract is a self-contained unit with declared
state, operations, invariants, and a contention model. There is no inheritance, no import system
for mutable state, and no implicit globals.
contract Name<TypeParams> { version: u32 // required, monotonic contention: model // required capability ... // zero or more state { ... } // exactly one invariant ... // zero or more operation ... // one or more view ... // zero or more }
Version
Every contract has a version field. It must be a positive integer and must increase
monotonically between deployments. The runtime uses this for state migration.
Type Parameters
Contracts can be generic. Type parameters appear after the name and can carry trait bounds
like copy and drop which control whether values of that type can be
duplicated or silently destroyed.
contract Token<Symbol: copy + drop> { // Symbol can be duplicated and dropped freely // useful for marker types like "USDC" or "ETH" }
State
The state block declares every piece of persistent storage the contract owns.
All state is explicit — there are no hidden storage slots, no dynamic allocation outside
what you declare.
state { total_supply: u256, balances: Map<address, u256>, frozen: Set<address>, owner: address, }
State can also be parameterized by the contention key. If your contract declares
contention: per<Pair>, you can have state scoped to each partition:
contention: per<Pair> state<Pair> { bids: SortedMap<u256, Vec<Order>>, asks: SortedMap<u256, Vec<Order>>, }
Operations
Operations are the write-path of your contract. They are the only way to mutate state. Every operation must declare exactly what it touches through clauses.
operation transfer(to: address, amount: u256) requires: self.balances[caller] >= amount mutates: self.balances[caller], self.balances[to] emits: Transfer { self.balances[caller] -= amount; self.balances[to] += amount; emit Transfer { from: caller, to, amount }; }
Clauses
Clauses are the contract between you and the compiler. Break it and your code doesn't compile.
| Clause | Purpose | Enforcement |
|---|---|---|
requires: |
Precondition that must hold before execution | Checked at entry; transaction reverts if false |
mutates: |
Exhaustive list of state fields the operation writes | Compiler rejects writes to undeclared fields |
emits: |
Events the operation may emit | Compiler rejects undeclared emits |
consumes: |
Linear resources destroyed by this operation | Resource must be moved in and destroyed exactly once |
mutates: clause.
There is no unsafe block, no assembly inline, no backdoor. This is what enables
the scheduler to parallelize without speculation.
Resources
Resources are Cudo's linear types. A resource must be used exactly once — you can't copy it, you can't silently drop it. This is how Cudo eliminates double-spend at the type level.
resource Coin { amount: u256 } operation pay(coin: Coin, to: address) consumes: coin { send(coin, to); // coin is moved here // send(coin, attacker); <-- compile error: used after move }
Resources follow three rules:
- No copy. You cannot duplicate a resource.
let b = a;movesa, it doesn't copy it. - No implicit drop. If a resource goes out of scope without being consumed, the compiler errors. No "forgotten" payments.
- Explicit destroy. To intentionally destroy a resource (e.g., burning tokens), use
destroy.
operation burn(coin: Coin) consumes: coin mutates: self.total_supply { self.total_supply -= coin.amount; destroy coin; // explicitly destroyed }
Capabilities
Capabilities replace role-based access control with type-level permissions. A capability isn't a boolean check — it's a type constraint that the compiler enforces. If you don't have the capability, the compiler won't let you call the operation.
capability Minter; capability Freezer; operation mint(to: address, amount: u256) requires: caller has Minter mutates: self.total_supply, self.balances[to] { self.total_supply += amount; self.balances[to] += amount; } operation freeze(account: address) requires: caller has Freezer mutates: self.frozen { self.frozen.insert(account); }
Capabilities can be granted, revoked, and combined. They compose naturally — an operation can require multiple capabilities, and the compiler proves each caller has all of them at the call site.
Contention
Every contract declares a contention model that tells the scheduler how to
parallelize transactions. This isn't an optimization hint — it's a compiler-enforced
guarantee about which operations can run concurrently.
| Model | Syntax | Parallelism | Use Case |
|---|---|---|---|
per<K> |
contention: per<address> |
Operations on different keys run in parallel | Tokens, balances, per-user state |
owned |
contention: owned |
Single-owner, strictly sequential | Wallets, personal vaults |
global |
contention: global |
Total ordering, no parallelism | Governance, rare singleton contracts |
contract Token { version: 1 contention: per<address> // Alice→Bob ‖ Carol→Dave // transfers between non-overlapping address sets // execute on different threads simultaneously }
Invariants
Invariants are properties that the compiler proves hold after every operation. This is not testing. This is not SMT solving. The compiler structurally verifies that every possible execution path preserves the invariant.
invariant conservation: self.total_supply == self.balances.values().sum() invariant non_negative: self.balances.values().all(|b| b >= 0)
If the compiler can't prove that an operation preserves an invariant, it rejects the program with a concrete counter-example showing which execution path breaks it.
Views
Views are the read-only path. They can access state but cannot mutate it. Views don't
need mutates: clauses because they don't touch anything. They're free to call
without gas considerations.
view balance_of(account: address) -> u256 { self.balances[account] } view is_frozen(account: address) -> bool { self.frozen.contains(account) }
Events
Events must be declared and listed in the emits: clause. This means the compiler
knows every possible event an operation can produce, enabling indexers to be generated
automatically.
event Transfer { from: address, to: address, amount: u256, } operation transfer(...) emits: Transfer { // ... emit Transfer { from: caller, to, amount }; }
Primitive Types
| Type | Description | Size |
|---|---|---|
u8 .. u256 | Unsigned integers (8 to 256 bits) | 1-32 bytes |
i8 .. i256 | Signed integers | 1-32 bytes |
bool | Boolean | 1 byte |
address | 32-byte account address | 32 bytes |
bytes | Dynamic byte array | variable |
string | UTF-8 string | variable |
Arithmetic
All arithmetic is checked by default. Overflow aborts the transaction. If you need wrapping arithmetic, opt in explicitly:
| Operator | Checked | Wrapping |
|---|---|---|
| Add | + | +% |
| Sub | - | -% |
| Mul | * | *% |
Generics
Type parameters can carry trait bounds that control value semantics:
| Bound | Meaning |
|---|---|
copy | Value can be duplicated (implicit copy on assignment) |
drop | Value can be silently destroyed when it goes out of scope |
| neither | Value is a linear resource — use exactly once |
// Symbol can be freely copied and dropped contract Token<Symbol: copy + drop> { ... } // Coin has no bounds — it's a linear resource resource Coin { amount: u256 }
Linear Types
Linear types are the foundation of Cudo's safety model. Any type without copy
and drop bounds is linear: it must be used exactly once. The compiler tracks
ownership through every branch, loop, and function call.
The compiler catches:
- Use after move: Using a resource after it's been sent somewhere (double-spend)
- Unused resource: A resource that goes out of scope without being consumed (lost funds)
- Branch asymmetry: A resource consumed in one
ifbranch but not the other
Collections
| Type | Description |
|---|---|
Map<K, V> | Key-value mapping (hash map semantics) |
Set<T> | Unique value set |
Vec<T> | Dynamic array |
SortedMap<K, V> | Ordered map (B-tree semantics) |
Collections declared in state are persisted on-chain. Local collections in operation
bodies are transient and discarded after execution.
CLI Reference
| Command | Description |
|---|---|
cudo check <file> | Type-check and verify invariants. Reports contention model. |
cudo check <file> --schedule | Type-check + show parallelism schedule. |
cudo compile <file> | Compile to dual-ISA binary (RISC-V + Wasm). |
cudo run <file> --op <name> | Execute an operation in a local sandbox. Reports gas usage. |
cudo inspect <file> | Print contract metadata: state layout, operations, contention. |
cudo prove <file> | Export invariant proofs as verifiable artifacts. |
cudo new <name> | Scaffold a new contract project. |
Error Catalog
| Code | Name | Description |
|---|---|---|
E0301 | Resource used after move | A linear resource was used after it was already moved. Double-spend attempt. |
E0302 | Resource dropped without consume | A linear resource went out of scope without being explicitly consumed or destroyed. |
E0303 | Undeclared state mutation | Operation body writes to a state field not listed in its mutates: clause. |
E0304 | Invariant violation | Compiler cannot prove that an operation preserves a declared invariant. |
E0305 | Undeclared event emission | Operation emits an event not listed in its emits: clause. |
E0401 | Missing capability | Caller does not have the required capability for this operation. |
E0501 | Contention conflict | Operation accesses state outside its declared contention partition. |
E0601 | Integer overflow | Arithmetic would overflow. Use wrapping operators (+% -% *%) if intentional. |