Phased Execution Model
Given phases P₁, P₂, …, Pₙ in declaration order, each phase runs three steps before the next begins:
for each phase Pᵢ:
1. Compute transforms tagged Pᵢ
(all expressions see state_{Pᵢ₋₁} — the pre-phase snapshot)
2. Apply those transforms atomically → state_Pᵢ
3. Execute effects of Pᵢ
(effects see post-transform state_Pᵢ)
Members without a transform at Pᵢ keep their value from Pᵢ₋₁.
Atomicity inside a phase
Transforms in the same phase are computed from the same pre-phase snapshot and written together. Two members can swap without observing a half-updated state:
m_a: u64 { in example() => p1: m_b }
m_b: u64 { in example() => p1: m_a }
After p1, m_a and m_b have exchanged their previous values.
var capture
A phase action may bind the return value of a synchronous external call:
fetch: [
var bal = balanceOf(who) ~> m_token;
]
The var is in scope from the moment its ~> runs through all later
phases of the same invocation. Later transforms and per-phase where
clauses may read it; earlier phases and the route-level where may not.
Per-phase where (V27 / V28)
Attach a precondition to a later phase with:
phaseName where (cond) : throw N : [ … ]
The condition lowers to a require(...) at the top of that phase —
after previous phases have finished (so earlier vars are bound) and
before this phase’s transforms or effects.
withdraw(who: address) => [
fetch: [
var bal = balanceOf(who) ~> m_token;
]
act where bal > 0 : throw 401 : [
]
]
Scoping rules:
| Clause | May reference vars from… | Error if violated |
|---|---|---|
Route-level where | Never (params + pre-existing state only) | V27 |
Per-phase where on Pᵢ | Strictly earlier phases P₁…Pᵢ₋₁ | V28 |
Move checks that depend on a captured value onto the phase that needs
them — do not put them on the route-level where.
Route-level guards still run first
A route-level where runs once, before any phase. If it fails, no phase
executes and no transforms apply. Use it for conditions over parameters
and members that already exist at entry.
Reentrancy across phase boundaries (EVM)
Unphased routes keep CEI: all SSTOREs before any CALL.
Phased routes intentionally allow CALLs between groups of storage writes. That is useful (cross-entity reads, flash callbacks) but means a callee can re-enter before later phases run:
P₁ transforms → P₁ effects (external call) → P₂ transforms → …
On reentry, later phases of the outer invocation have not applied yet. Design accordingly:
- Prefer unphased routes when you only need CEI.
- When using phases, put critical authorization and balance updates in
phases that complete before the external call, or gate later phases
with per-phase
whereon captured post-call state (as Uniswap V2swap’scheck:phase does for the K-invariant).
See Phased Routes for when to introduce phases at all.