Deterministic Addresses
On EVM, every entity in the project gets a predictable CREATE2 address
derived from its identity arguments. Cross-entity sends, from checks, and
deploy all share that same prediction — no hand-rolled factory.
name: det-messaging
target: evm
output_dir: build/
sources:
- det_guardian.cam
- det_locker.cam
CREATE2 and CambrianFactory
The EVM backend emits a project-local CambrianFactory. Every entity is
deployed by it with CREATE2, so an address is derived from:
- the factory’s own address,
- a fixed salt of
0, - the entity’s creation code plus its constructor arguments — the factory address followed by the identity values.
The factory address is not known when code is generated. Instead, each
entity stores the factory that deployed it (an immutable _factory) and asks
it for predictions: Entity.address(args) lowers to a call to the factory’s
generated predict<Entity>(args). Every entity deployed by the same factory
therefore agrees on every other entity’s address, computed from identity
fields alone.
Authors never write a custom factory.
Entity.address(args) and addressOf
Prefer the dot form. The addressOf(Entity.state(args)) spelling exists as
well; both lower to the same CREATE2 prediction:
acceptPing() ~> Locker.address(locker_id)
acceptPing() ~> addressOf(Locker.state(locker_id))
Arity equals the identity-member count (Identity Members).
Singletons use Entity.address() with no arguments.
Use Entity.address(…) anywhere you need a predicted address: send
destinations, member initializers, from comparisons.
from Entity(args)
On EVM, sender authentication compares msg.sender to the
same CREATE2 expression:
acceptPing()
from Guardian(m_id)
=> []
Here Locker only accepts pings from the Guardian instance whose
identity matches this locker’s m_id. from Entity(args) arity must match
the target entity’s identity-member count (V33).
deploy Entity(…)
deploy goes through CambrianFactory:
- CREATE2-deploy with the factory address and identity args as constructor
arguments (address matches
Entity.address(…)), forwarding any attached value. - Call a generated, factory-guarded
initialize(…)for non-identity constructor parameters.
spawnLocker(locker_id: u64) => [
deploy Locker(locker_id)
]
createPair(tokenA: address, tokenB: address) => [
deploy UniswapV2Pair(min_addr(tokenA, tokenB),
max_addr(tokenA, tokenB))
]
Only the factory owner (bootstrap / tests) or addresses already deployed
by this factory may call deployX. EOAs cannot occupy identity CREATE2
slots directly; child deploys go through entity routes the factory already
knows. initialize() requires msg.sender == _factory and rejects
re-initialization.
Summary
| Expression | Role |
|---|---|
Entity.address(args) | Predict CREATE2 address from identity |
addressOf(Entity.state(args)) | Equivalent spelling; same CREATE2 |
from Entity(args) | msg.sender == that CREATE2 address |
deploy Entity(args) | Factory CREATE2 + guarded initialize |
Keep identity, addressing, authentication, and deployment aligned — one argument list, one address, everywhere.