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

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:

  1. var capture — a synchronous return from an external call that later transforms or guards depend on.
  2. Interleaving — an effect must run between state updates (or between other effects) rather than after all of them.
  3. Snapshot before overwrite — a let, if condition, emit, or return needs 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

NeedPattern
Read a remote view, then gate local writesfetch: with var x = … ~> dest, then act where …
Bind (log, branch on) a value the route resetssnap: with let x = m_…;, then pay: with the transform and the actions
Optimistic transfer, then callback, then checkThree phases (see Uniswap V2 swap)
Effect between two groups of transformsExplicit 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

  1. Phases and their order live exclusively in the route body (tag: […]).
  2. In a phased route, every participating transform must carry a phase tag.
  3. In an unphased route, transforms must not carry phase tags.
  4. Untagged actions are illegal inside a phased body.
SituationError
Transform tag missing from the routeunknown phase
Phased route, transform without a tagtransform must specify a phase
Unphased route, transform with a tagroute has no phases
Untagged action in a phased bodyuntagged action

Cross-phase where / var rules are covered in Execution Model.