Phased Routes
Most routes are unphased: every member transform runs as one atomic
step, then every action (~>, deploy, emit, …) runs afterward. That
covers the common case and matches the checks–effects–interactions (CEI)
discipline on EVM. Arguments of sends, call, and deploy still carry the
values from before the transforms, so an unphased route can reset a balance
and pay out the old amount.
Reach for named phases only when you need something the unphased model cannot express:
varcapture — a synchronous return from an external call that later transforms or guards depend on.- Interleaving — an effect must run between state updates (or between other effects) rather than after all of them.
- Snapshot before overwrite — a
let,ifcondition,emit, orreturnneeds a member’s old value (those actions read the values after all transforms; only effect arguments read the old ones).
Otherwise keep the route unphased. Fewer phases are easier to read and audit.
Syntax
A phased route body is a sequence of tag: [ … ] blocks. Phases run in
declaration order:
castVote(proposal_id: U256, support: u8) => [
read: [
var weight = getVotes(msg::sender) ~> m_token;
]
tally where weight > 0 : throw 100 : [
]
]
(m_token is an Address<Token> member; a var capture needs a typed
destination.)
Member transforms that participate in a phased route must name the same phase tag:
m_for: HashMap<U256, U256> {
in castVote(proposal_id, support) => tally: {
if support == 1 {
m_for.update(proposal_id, m_for[proposal_id] + weight)
} else {
m_for
}
}
}
Prefer unphased on EVM
For an unphased route the EVM backend flushes all member SSTOREs before any external call. You get CEI without writing phases:
transfer(to: address, amount: U256)
where amount > 0 : throw 1
=> [
emit Transfer(msg::sender, to, amount);
]
Balances update in the member transforms; emit (and any ~>) runs after
storage is consistent. Prefer this shape whenever you do not need a
captured return value mid-route.
When phases are the right tool
| Need | Pattern |
|---|---|
| Read a remote view, then gate local writes | fetch: with var x = … ~> dest, then act where … |
| Bind (log, branch on) a value the route resets | snap: with let x = m_…;, then pay: with the transform and the actions |
| Optimistic transfer, then callback, then check | Three phases (see Uniswap V2 swap) |
| Effect between two groups of transforms | Explicit empty phases (tag: []) so transforms can attach |
The snapshot pattern — snap only binds, pay writes storage and then
sends, so it also keeps checks-effects-interactions order. Here the old amount
is needed by the emit as well as the send, so it goes through a let bound
before the reset:
entity Payouts {
event Claimed(indexed who: address, amount: U256);
routes {
claim()
where m_pending[msg::sender] > 0 : throw 1
=> [
snap: [
let amount = m_pending[msg::sender];
]
pay: [
~> msg::sender with { value: amount }
emit Claimed(msg::sender, amount);
]
]
}
m_pending: HashMap<address, U256> {
in claim() => pay: m_pending.insert(msg::sender, 0)
}
}
A phase that only applies transforms still appears in the route body as
tag: []. The route body is the source of truth for phase names and order;
transforms may only reference tags declared there.
Declaration rules
- Phases and their order live exclusively in the route body (
tag: […]). - In a phased route, every participating transform must carry a phase tag.
- In an unphased route, transforms must not carry phase tags.
- Untagged actions are illegal inside a phased body.
| Situation | Error |
|---|---|
| Transform tag missing from the route | unknown phase |
| Phased route, transform without a tag | transform must specify a phase |
| Unphased route, transform with a tag | route has no phases |
| Untagged action in a phased body | untagged action |
Cross-phase where / var rules are covered in
Execution Model.