Escrow
Two-party escrow with an arbiter and a deadline. Prefer the enum-based
contracts/escrow_v2.cam; contracts/escrow.cam is the same machine with
u8 state codes.
State machine
Created --fund()--> Funded
Funded --release()--> Released (pay seller)
Funded --refund()--> Refunded (pay buyer)
Funded --claimTimeout()--> Refunded (after deadline)
Funded --dispute()--> Disputed
Disputed --resolve(pct)--> Resolved (split by %)
Enum version (excerpt)
type Amount = U256
type Timestamp = u64
pure fn is_party(sender: address, buyer: address, seller: address) -> bool {
sender == buyer || sender == seller
}
entity EscrowV2 {
enum State {
Created,
Funded,
Released,
Refunded,
Disputed,
Resolved
}
routes {
constructor(buyer: address, seller: address, arbiter: address,
amount: Amount, deadline: Timestamp) => []
fund()
where m_state == State::Created : throw 10 &&
msg::sender == m_buyer : throw 11 => []
release()
where m_state == State::Funded : throw 10 &&
msg::sender == m_buyer : throw 11 => [
~> m_seller
]
refund()
where m_state == State::Funded : throw 10 &&
msg::sender == m_seller : throw 12 => [
~> m_buyer
]
claimTimeout()
where m_state == State::Funded : throw 10 &&
msg::sender == m_buyer : throw 11 &&
msg::timestamp > m_deadline : throw 16 => [
~> m_buyer
]
dispute()
where m_state == State::Funded : throw 10 &&
is_party(msg::sender, m_buyer, m_seller) : throw 14 => []
resolve(to_buyer_pct: u8)
where m_state == State::Disputed : throw 10 &&
msg::sender == m_arbiter : throw 13 => [
let buyer_amount = m_amount * to_buyer_pct as Amount / 100;
let seller_amount = m_amount - buyer_amount;
if buyer_amount + seller_amount == m_amount => [
~> m_buyer
~> m_seller
]
]
getStatus() -> (u8, Amount, Timestamp) => [
let state_code = match m_state {
State::Created => 0,
State::Funded => 1,
State::Released => 2,
State::Refunded => 3,
State::Disputed => 4,
State::Resolved => 5
};
return(state_code, m_amount, m_deadline)
]
}
m_state: State {
in constructor(_, _, _, _, _) => State::Created
in fund() => State::Funded
in release() => State::Released
in refund() => State::Refunded
in claimTimeout() => State::Refunded
in dispute() => State::Disputed
in resolve(_) => State::Resolved
}
m_resolution: Option<String> {
in constructor(_, _, _, _, _) => none
in resolve(_) => some("Resolved by arbiter")
}
}
Buyer / seller / arbiter / amount / deadline members are set once in
constructor (full file in the repo). Plain ~> dest transfers value;
the arbiter’s resolve splits by percentage with a conservation check.
Why enums
escrow.cam (u8) | escrow_v2.cam (enum) | |
|---|---|---|
| State type | magic numbers | State::Funded |
| Queries | return raw u8 | match to a code if needed |
| Extra state | — | Option<String> resolution note |
Same guards and payouts; the enum version is what you want for new code.
Governor and Timelock use the same enum + member-transform pattern for
proposal / operation lifecycles.
Tests in the workspace
| File | Role |
|---|---|
contracts/escrow_v2.test.cam | Unit tests for the enum escrow |
contracts/escrow.test.cam | Same machine with u8 state codes |
contracts/escrow_two_vaults.invariant.cam | Multi-entity invariant example |
List them in project.yaml with target: evm to emit Foundry tests — see
Counter.