Properties and Fuzz
A property is Cambrian’s abstract, parameterised statement about an entity. It is the canonical construct for property-based testing and for Lean specifications.
Shape
- Typed parameters attach to the property name (before
for). - The body holds logical steps only:
msg/sysorctx,let,assume,call,expect*. - Concrete
testand randomisedfuzzinstances nest inside the property.
property "increment is monotonic" (amount: u64) for Counter with { m_count: 0 } {
assume amount % 2 == 0
call increment(amount)
expect state { m_count: amount }
test "even-4" { amount: 4 }
fuzz "small" { amount in 0..1000 }
#[runs(2000)] fuzz "wide" { amount in 0..=1000 } with { m_count: 5 }
}
Notes:
- Write
for Counter, neverfor Counter(amount: u64). - Sampling
bounds belong in fuzz instances, not the property body (T11). - Instance forms:
test { p: value, … }(every param fixed) andfuzz { p in lo..hi, … }(ranges; omitted params use the full type range). - Property-level
withis the default initial state; instances may override with their ownwith. - Instance attributes include
#[runs(N)](fuzz),#[tag("…")],#[skip_from]. - A property with no instances still emits output: full-range fuzz if it has params, or a deterministic test if not. Lean always emits one theorem per property.
assume vs bound
| Construct | Role | Where | Reaches Lean? |
|---|---|---|---|
assume <bool> | Logical precondition | property body | yes (→ hypothesis) |
bound x in lo..hi | Sampling range | fuzz instance | no |
bound x in lo..=hi | Inclusive range | fuzz instance | no |
Lean quantifies over the full parameter type with only genuine assumes — fuzz ranges never weaken the theorem.
Forall marker *
property "reset zeroes from any start" for Counter with { m_count: * } {
call reset()
expect state { m_count: 0 }
}
property "holds for any state" for Counter with { * } {
call reset()
expect state { m_count: 0 }
}
Form in with | Meaning |
|---|---|
field: v | pin member |
field: * | forall over member |
* (bare) | forall over all members (explicit pins still win) |
with vs ctx
Entity state and call context are separate records:
property "incr from any sender" (amount: u64) for Counter
with { m_count: 0 }
ctx { msg::sender: * } {
call increment(amount)
expect state { m_count: amount }
fuzz { amount in 0..10 }
}
ctx form | Meaning |
|---|---|
msg::x: v / sys::x: v | pin |
msg::x: * / sys::x: * | forall / sample |
Allowed context names: msg::{sender,value}, sys::{now,timestamp,chainid,block_number,balance} (T20). Context markers inside with → T22.
Per-target:
| Target | Forall state / ctx becomes… |
|---|---|
| Lean | ∀-bound seeding world / MsgCtx |
| fuzz (EVM) | Extra sampled input |
concrete test | State forall falls back to default (W8); ctx forall dropped |
How Foundry runs properties
On --target evm, each nested instance becomes a runnable harness:
fuzzinstance → fuzz case withbound p in lo..hi, then the logical body.testinstance → concrete test withlet p = value;assumes are dropped (values are presumed valid).
Lean keeps the property as a single theorem instead (see Lean-EVM).
How preconditions lower:
| Backend | assume | bound |
|---|---|---|
| Foundry | vm.assume | StdUtils.bound |
Unsupported fuzz parameter types warn with W7 rather than failing the build.
Project-wide fuzz knobs
fuzz:
runs: 256
seed: 0
shrink: true
max_local_rejects: 1024
Threaded into Foundry [profile.default.fuzz].