Two-party escrow — funds move only when BOTH sides say so, and never leak
Classified-ad escrow fraud works one way: the "escrow agent" releases the buyer's money before the seller delivers, or refunds after releasing — paying the same dollars out twice. The FBI's IC3 logs thousands of these a year. The fix is structural: release/refund are TERMINAL states of one machine (no path back, proven by `soma verify`), release requires both confirmations, and the held ledger refuses negative balances so the same cent cannot leave twice. Conservation (held + paid == funded) is checked on every read.
A Soma program from the escrow_finance part of the verified corpus: 117 lines, cells Escrow, EscrowTests. It passes soma check with soma 3.2.4.
How it is built
The cell Escrow declares 5 signals in its face: open_escrow(id: String, amount: Int) -> Map, confirm(id: String, party: String) -> Map, release_funds(id: String) -> Map, refund_buyer(id: String) -> Map and balances(id: String) -> Map; keeps 4 memory slots: held (Map<String, Int>, persistent and consistent) under the invariant held >= 0, paid (Map<String, Int>, persistent and consistent) under the invariant paid >= 0, funded (Map<String, Int>, persistent and consistent) under the invariant funded >= 0 and oks (Map<String, Int>, persistent and consistent). Its state machine deal starts in created and allows 3 transitions: created -> funded_state, funded_state -> released and funded_state -> refunded. refunded and released are terminal. Handlers: open_escrow (line 45), confirm (line 54), release_funds (line 63), refund_buyer (line 76) and balances (line 84).
The file also holds 1 test cell (EscrowTests) with 16 assertions, run by soma test.
The source
two_party_escrow.cell
// ============================================================================
// TWO-PARTY ESCROW — funds move only when BOTH sides say so, and never leak
// ============================================================================
//
// Classified-ad escrow fraud works one way: the "escrow agent" releases the
// buyer's money before the seller delivers, or refunds after releasing —
// paying the same dollars out twice. The FBI's IC3 logs thousands of these
// a year. The fix is structural: release/refund are TERMINAL states of one
// machine (no path back, proven by `soma verify`), release requires both
// confirmations, and the held ledger refuses negative balances so the same
// cent cannot leave twice. Conservation (held + paid == funded) is checked
// on every read.
// ============================================================================
cell Escrow {
face {
signal open_escrow(id: String, amount: Int) -> Map
signal confirm(id: String, party: String) -> Map
signal release_funds(id: String) -> Map
signal refund_buyer(id: String) -> Map
signal balances(id: String) -> Map
}
memory {
// money still in the vault — can never go negative
held: Map<String, Int> [persistent, consistent]
invariant held >= 0
// money that has left (to seller OR back to buyer)
paid: Map<String, Int> [persistent, consistent]
invariant paid >= 0
funded: Map<String, Int> [persistent, consistent]
invariant funded >= 0
oks: Map<String, Int> [persistent, consistent]
}
// released and refunded are TERMINAL: once money moves, there is no
// state that lets it move again. The double-payout has no path.
state deal {
initial: created
created -> funded_state
funded_state -> released
funded_state -> refunded
}
on open_escrow(id: String, amount: Int) {
if amount <= 0 { return map("ok", false, "reason", "amount must be positive") }
transition(id, "funded_state")
funded.set(id, amount)
held.set(id, amount)
paid.set(id, 0)
return map("ok", true, "state", get_status(id), "held", amount)
}
on confirm(id: String, party: String) {
if party != "buyer" && party != "seller" {
return map("ok", false, "reason", "unknown party")
}
let k = "{id}:{party}"
oks.set(k, 1)
return map("ok", true, "party", party)
}
on release_funds(id: String) {
let b = oks.get("{id}:buyer") ?? 0
let s = oks.get("{id}:seller") ?? 0
if b != 1 || s != 1 {
return map("ok", false, "reason", "need BOTH confirmations (buyer={b} seller={s})")
}
transition(id, "released") // raises unless currently funded
let amt = held.get(id) ?? 0
held.set(id, 0)
paid.set(id, amt)
return map("ok", true, "state", get_status(id), "paid_to_seller", amt)
}
on refund_buyer(id: String) {
transition(id, "refunded") // raises if already released
let amt = held.get(id) ?? 0
held.set(id, 0)
paid.set(id, amt)
return map("ok", true, "state", get_status(id), "refunded", amt)
}
on balances(id: String) {
let h = held.get(id) ?? 0
let p = paid.get(id) ?? 0
let f = funded.get(id) ?? 0
return map("held", h, "paid", p, "funded", f, "conserved", h + p == f)
}
}
cell test EscrowTests {
rules {
assert open_escrow("e1", 500).ok == true
// one confirmation is NOT enough to move money
assert confirm("e1", "buyer").ok == true
assert release_funds("e1").ok == false
assert balances("e1").held == 500
// the vault cannot be driven negative, even by buggy code
assert_fails held.set("e1", -1)
// both sides agree -> money moves exactly once
assert confirm("e1", "seller").ok == true
assert release_funds("e1").ok == true
assert balances("e1").held == 0
assert balances("e1").paid == 500
assert balances("e1").conserved == true
// THE FRAUD: refund after release — terminal state, no path
assert_fails transition("e1", "refunded")
assert_fails transition("e1", "funded_state")
// a separate deal can still refund (and then never release)
assert open_escrow("e2", 300).ok == true
assert refund_buyer("e2").ok == true
assert_fails transition("e2", "released")
assert balances("e2").conserved == true
}
}What the toolchain says
$ soma check two_party_escrow.cell
✓ All checks passed.$ soma test two_party_escrow.cell
test EscrowTests ...
✓ assert open_escrow("e1", 500).ok == true
✓ assert confirm("e1", "buyer").ok == true
✓ assert release_funds("e1").ok == false
✓ assert balances("e1").held == 500
✓ assert_fails held.set("e1", -1) — raised memory invariant violated on 'held': held >= 0 — rejected write of -1 (key "e1"); the slot is unchanged
✓ assert confirm("e1", "seller").ok == true
✓ assert release_funds("e1").ok == true
✓ assert balances("e1").held == 0
✓ assert balances("e1").paid == 500
✓ assert balances("e1").conserved == true
✓ assert_fails transition("e1", "refunded") — raised invalid transition: released → refunded. Current state: 'released'. Valid targets: []
✓ assert_fails transition("e1", "funded_state") — raised invalid transition: released → funded_state. Current state: 'released'. Valid targets: []
✓ assert open_escrow("e2", 300).ok == true
✓ assert refund_buyer("e2").ok == true
✓ assert_fails transition("e2", "released") — raised invalid transition: refunded → released. Current state: 'refunded'. Valid targets: []
✓ assert balances("e2").conserved == true
16 tests: 16 passed, 0 failed$ soma verify two_party_escrow.cell
Verifying two_party_escrow.cell...
State machine 'deal': 4 states, initial 'created'
States: [created, funded_state, refunded, released]
✓ 4 states, 3 transitions
✓ all states reachable from 'created'
✓ terminal states: [released, refunded]
✓ no deadlocks
✓ liveness: every state can eventually reach a terminal state
✓ think-isolated: CTL safety properties hold regardless of LLM output (3 handlers, 3 literal transitions, 0 dynamic)
✓ termination: all 5 handlers structurally terminate
✓ refinement: handler `open_escrow` ⟶ {funded_state}
✓ refinement: handler `release_funds` ⟶ {released}
✓ refinement: handler `refund_buyer` ⟶ {refunded}
· transition sources: `open_escrow`, `release_funds`, `refund_buyer` call transition(id, target) without declaring the source — the runtime refuses a move the graph forbids, but the proof does not see where the handler starts; `transition(id, from, to)` names the edge (checked by `soma check`, refused at run time from any other state)
Escrow/invariants:
✓ invariant held >= 0 — writer 'open_escrow' proven by induction (writes amount)
✓ invariant held >= 0 — writer 'refund_buyer' proven (writes 0)
✓ invariant held >= 0 — writer 'release_funds' proven (writes 0)
✓ invariant paid >= 0 — writer 'open_escrow' proven (writes 0)
✓ invariant paid >= 0 — writer 'refund_buyer' proven by induction (writes amt)
✓ invariant paid >= 0 — writer 'release_funds' proven by induction (writes amt)
✓ invariant funded >= 0 — writer 'open_escrow' proven by induction (writes amount)
17 passed, 0 warnings, 0 failures
Temporal properties for 'deal':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK