Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. CREATE2-deploy with the factory address and identity args as constructor arguments (address matches Entity.address(…)), forwarding any attached value.
  2. 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

ExpressionRole
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.