soma.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.

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.

Generated from soma describe on this file.

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

Run on a fresh copy of the program at build time; the outputs are pasted unedited.

$ 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

Builtins used

get_status, map, transition

Source file: /corpus/escrow_finance/two_party_escrow.cell · examples/corpus/escrow_finance/two_party_escrow.cell