Solidity-EVM
--target evm lowers a Cambrian program to flat Solidity under src/, with an optional Foundry test harness. The pragma is ^0.8.24. Semantics follow the EVM domain: synchronous calls, ETH balances, and CREATE2 addresses (every entity is deployed through the generated CambrianFactory).
Output layout
Single file, entity only:
out/
.cambrian-manifest.json # generated files + SHA-256 hashes
src/Counter.sol # entity contract + CambrianFactory
Single file with test / property / invariant blocks, or a multi-file project (project.yaml):
out/
.cambrian-manifest.json
src/_<project>_project.sol # combined program (+ CambrianFactory)
src/<Entity>.sol # thin import stubs per entity
test/… # Foundry tests
foundry.toml
setup.sh # installs forge-std (forge install --no-git)
cambrian-transpiler --project project.yaml # output_dir comes from the YAML
cd <output_dir> && bash setup.sh && forge test
Never invoke solc directly for Cambrian projects — use forge build / forge test.
Foundry configuration
The emitter writes foundry.toml with [profile.default] plus tiered profiles:
| Profile | Role |
|---|---|
default | Full local runs (fuzz / invariant knobs from project.yaml) |
cambrian | Cheap CI (fuzz.runs ≈ 100, invariant.runs ≈ 50) |
cambrian_night | Overnight stress (thousands of runs / deep traces) |
Override via foundry: and top-level fuzz: / invariant: blocks in project.yaml. Run with FOUNDRY_PROFILE=cambrian forge test.
Deterministic addresses and CambrianFactory
On EVM:
- The backend emits a
CambrianFactory(andICambrianFactory) with onedeploy<Entity>(…)/predict<Entity>(…)pair per entity; deploys use CREATE2. Every entity constructor takes the factory address. - An init route with parameters splits into a deploy-time constructor stub plus a factory-guarded
initialize(…), whichdeploy<Entity>calls. Mark the init route#[factory_only]and pass recipients (owner, mint beneficiary) as explicit parameters — under the factory,msg::senderin the init route is the factory. Entity.address(args)andaddressOf(Entity.state(args))lower to the same CREATE2 prediction used in sends andfromclauses.
Singletons (no identity members) get a fixed address; Entity.address() takes no arguments.
from Entity(args) arity must match the target entity’s identity-member count (rule V33).
Payable auto-detection
There is no Cambrian payable keyword. Solidity payable is inferred:
- Any route whose body (or reachable AST) reads
msg::valuebecomespayable. receiveis alwayspayable;fallbackispayableonly when it readsmsg::value.- Constructors (and the factory’s
deploy<Entity>) arepayableby default sodeploy … with { value: … }works. Setevm.allow_constructor_payable: falseinproject.yamlto make them non-payable (the factory then deploys withvalue: 0).
Events and errors (ABI)
Cambrian event / error declarations become Solidity ABI items:
event Transfer(indexed src: address, indexed dst: address, amount: U256);
error InsufficientBalance(have: U256, need: U256);
entity Wallet {
routes {
deposit(amount: U256) => []
withdraw(amount: U256)
where (m_balance >= amount) : throw InsufficientBalance(m_balance, amount)
=> [
emit Transfer(msg::sender, msg::sender, amount);
]
}
m_balance: U256 {
in deposit(amount) => m_balance + amount
in withdraw(amount) => m_balance - amount
}
}
emit Name(args)→emit Name(args);throw Name(args)→revert Name(args);- At most three
indexedevent parameters (V36). - Unphased routes keep SSTORE-before-CALL ordering (checks-effects-interactions).
Project knobs (EVM)
name: my-dapp
target: evm
foundry:
solc_version: "0.8.24"
# remappings, fuzz_runs, profiles, …
fuzz:
runs: 256
invariant:
runs: 256
depth: 50
See project.yaml and Testing backends.