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/Ferros-Network/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> { // Map values must be scalar in the v0 runtime — split records into // parallel maps. See "Map and SortedMap value-type restriction" below. bid_owner: SortedMap<u256, address>, bid_size: SortedMap<u256, u256>, ask_owner: SortedMap<u256, address>, ask_size: SortedMap<u256, u256>, }
Map and SortedMap value-type restriction
The v0 Cudo runtime stores one U256 per map slot. This
means Map<K, V> and SortedMap<K, V>
only support scalar V:
- Supported:
u8/u16/u32/u64/u128/u256,i8/…/i128,bool,address,hash/bytes32, enum variants encoded as small ints. - Not yet supported:
struct,Vec,Bytes,String,Option, tuples. A struct value silently truncates to a single zero slot on store — the compiler accepts it but reads returnUnitand the next arithmetic op aborts withexpected numeric value.
To model a record keyed by K, use parallel scalar maps
— one Map<K, T> per field. Since the storage model can't
distinguish absent from zero anyway, contains_key guards on first
writes are unnecessary — first writes upsert cleanly. Lifting
Map<K, Struct> into the language proper (compiler-side
desugaring at IR generation) is tracked work.
// Don't — struct value silently truncates: struct Account { balance: u256, margin_held: u256, } state { accounts: Map<address, Account>, // broken at runtime } // Do — one Map per field: state { account_balance: Map<address, u256>, account_margin_held: Map<address, u256>, }
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.
Revert Reasons
Operations revert with a reason string via assert(cond, "msg") or
abort("msg"). Reason strings propagate up through cross-contract calls
unchanged, which is usually fine — the reason explains what failed.
Sometimes a caller knows why the failure matters (e.g. "this path processes
the market maker's side, not the taker's") but can't easily thread that context into
the callee's reason.
with_reason("prefix:") { ... } wraps a block of statements so that any
revert raised inside has the given prefix prepended to its reason string. The block
does nothing on the success path. Reverts that already contain a : pass
through unchanged — a downstream contract that classified a fault authoritatively
isn't re-tagged by outer frames. Innermost prefix wins on nesting.
operation settle(mm_account: address, taker_account: address, size: u256, price: u256) { with_reason("mm:") { PositionManager.settle_mm_side(mm_account, size, price); } with_reason("taker:") { PositionManager.settle_taker_side(taker_account, size, price); } }
An off-chain fault classifier can now route "mm:insufficient_margin"
to an MM-slashing path and "taker:insufficient_balance" to a taker-slashing
path without the shared helper duplicating its body per role. The prefix is a compile-time
string literal; dynamic prefixes are not supported.
There is no catch. with_reason only relabels reverts; it does
not allow an operation to recover from one. Any revert still aborts the enclosing
operation and rolls back all state changes.
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 |
hash / bytes32 | Cryptographic commitment — output of keccak256. bytes32 is an alias for Solidity-style readability. | 32 bytes |
bytes | Dynamic byte array — output of abi_encode_packed | 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 | * | *% |
Bitwise and shifts
| Operator | Meaning | Notes |
|---|---|---|
& | Bitwise AND | |
| | Bitwise OR | |
^ | Bitwise XOR | |
~ | Bitwise NOT (unary) | |
<< | Left shift | Aborts if shift ≥ 256 |
>> | Right shift | Logical for unsigned, aborts if shift ≥ 256 |
Precedence (tightest first): unary, as, * / %, + -,
<< >>, &, ^, |, comparison,
&&, ||. Comparison binds tighter than bitwise — the
Rust convention, not the C one — so a & b == c parses as
a & (b == c).
Numeric casts
Cudo supports checked and wrapping casts between integer types, and zero-cost reinterpretation
between hash and u256. Integer literals are untyped until they land
in a concrete context (like a let annotation or a function argument), so
let x: i128 = -500 just works — no explicit cast needed.
| Syntax | Semantics |
|---|---|
x as T | Checked cast — aborts if x does not fit in the destination type |
x as% T | Wrapping cast — truncates (unsigned) or sign-extends (signed) |
let a: u32 = 1_000_000; let b: u64 = a as u64; // zero-extend let c: u8 = a as u8; // ABORT — 1_000_000 doesn't fit let d: u8 = a as% u8; // wrap — masks to low 8 bits let pnl: i128 = -500; // untyped literal coerces into i128 let loss: u256 = (-pnl) as u256; // 500 let h: hash = keccak256("hello"); let n: u256 = h as u256; // zero-cost reinterpret let h2: hash = n as hash;
else-if chains and match-as-expression
if supports else if chains, and match can appear in
expression position. Expression-form matches must be exhaustive — every variant of an
enum subject must be covered, booleans need both true and false, and
other subject types require a wildcard arm.
let bucket: u8 = if x < 100 { 1 } else if x < 10000 { 2 } else { 3 }; // match expression — exhaustive on the enum let code: u256 = match side { Side::Long => 1, Side::Short => 2, }; // wildcard catch-all let n: u8 = match color { Color::Red => 1, _ => 99, };
Array methods and iteration
Local arrays and Vec<T> values support .len(),
.contains(v), .get(i), .iter(), and
.iter().take(n). for loops accept any expression that evaluates to an
array at runtime — bare variables, iterator chains, or collection-key queries on state.
let v = [10, 20, 30]; let n: u256 = v.len(); // 3 // Iterate every element for x in v.iter() { self.total += x; } // Cap iteration at the first N elements for x in v.iter().take(2) { self.total += x; }
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.
Cryptography
Cudo ships two cryptographic primitives. Together they unlock on-chain Merkle tree construction, commitment schemes, and cross-chain proof verification — the building blocks of force-exit escape hatches and bridge attestations.
| Function | Signature | Description |
|---|---|---|
keccak256 | (bytes | string) -> hash | Ethereum Keccak-256 hash. Pure, deterministic, no side effects. Strings are hashed as their UTF-8 bytes. |
abi_encode_packed | (args...) -> bytes | Solidity-compatible packed serializer. Variadic. |
keccak256 is byte-for-byte compatible with
Ethereum's keccak256 opcode and Solidity's keccak256(bytes memory).
SHA3-256 (the NIST standard) uses a different padding byte and produces different output for
the same input. If you're verifying an EVM hash on Ferros, you want this one.
Encoding rules
abi_encode_packed mirrors Solidity's abi.encodePacked — the
output is byte-for-byte identical for every type that exists in both languages, which is what
makes cross-chain Merkle proofs work without bridge-side adapters.
| Cudo type | Encoded as |
|---|---|
u8 .. u256 | 32 bytes, big-endian (see integer width note below) |
i8 .. i256 | Same width as unsigned, two's complement |
bool | 1 byte: 0x00 or 0x01 |
address | 32 bytes — left-padded with 12 zero bytes, matching Solidity's bytes32(uint256(uint160(addr))) cast |
hash / bytes32 | 32 bytes |
bytes | Raw bytes, no length prefix, no padding |
string | Raw UTF-8 bytes, no length prefix |
bytes32
rather than address so the 32-byte encoding lines up. This gives one canonical
byte layout on both sides — no truncation, no padding ambiguity.
abi_encode_packed(u8(1)) emits 32 bytes, not 1. Solidity would
emit 1 byte for the same input. If you need byte-exact Solidity compatibility for sub-256-bit
widths, encode them yourself or upcast to u256 before packing.
Example: Merkle leaf for a force-exit escape hatch
A common pattern: produce a Merkle commitment over token balances so that off-chain
systems can prove inclusion without replicating all state. The committer reads balances
directly from the Token contract — the caller cannot supply forged values.
operation commit_balance(holder: address) requires: caller has SnapshotAttestation mutates: self.tree, self.latest_root external: Token.balance_of { let balance: u256 = Token.balance_of(holder); let leaf_bytes: bytes = abi_encode_packed(holder, balance); let leaf_hash: bytes32 = keccak256(leaf_bytes); self.tree.update_leaf(holder, leaf_hash); self.latest_root = self.tree.compute_root(); }
The Merkle tree itself uses Map<u32, bytes32> for node storage and
keccak256 for parent-node hashes — no further language primitives needed.
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. |