soma.cell

Client money segregation — the firm can only ever touch its own excess

MF Global, October 2011: as the firm's own liquidity died, roughly $1.6B of SEGREGATED customer funds were wired out to cover house obligations. The rule (CFTC 1.20) was clear; the enforcement was a spreadsheet. The structural version: the seg account tracks an `excess` slot — firm money deposited as a buffer — and EVERY house withdrawal draws ONLY on excess, with `excess >= 0` as the invariant. The wire that dips into customer money is a rejected write, even at 2am on the last day. Client claims are themselves a slot that cannot go negative (no over-withdrawal).

A Soma program from the escrow_finance part of the verified corpus: 100 lines, cells SegAccount, SegTests. It passes soma check with soma 3.2.4.

How it is built

The cell SegAccount declares 5 signals in its face: client_deposit(client: String, amount: Int) -> Map, client_withdraw(client: String, amount: Int) -> Map, firm_buffer_in(amount: Int) -> Map, firm_draw(amount: Int) -> Map and seg_status() -> Map; keeps 3 memory slots: claims (Map<String, Int>, persistent and consistent) under the invariant claims >= 0, excess (Map<String, Int>, persistent and consistent) under the invariant excess >= 0 and totals (Map<String, Int>, persistent and consistent) under the invariant totals >= 0. Handlers: client_deposit (line 36), client_withdraw (line 43), firm_buffer_in (line 51), firm_draw (line 57) and seg_status (line 66).

The file also holds 1 test cell (SegTests) with 15 assertions, run by soma test.

Generated from soma describe on this file.

The source

client_money_segregation.cell

// ============================================================================
// CLIENT MONEY SEGREGATION — the firm can only ever touch its own excess
// ============================================================================
//
// MF Global, October 2011: as the firm's own liquidity died, roughly $1.6B
// of SEGREGATED customer funds were wired out to cover house obligations.
// The rule (CFTC 1.20) was clear; the enforcement was a spreadsheet. The
// structural version: the seg account tracks an `excess` slot — firm money
// deposited as a buffer — and EVERY house withdrawal draws ONLY on excess,
// with `excess >= 0` as the invariant. The wire that dips into customer
// money is a rejected write, even at 2am on the last day. Client claims
// are themselves a slot that cannot go negative (no over-withdrawal).
// ============================================================================

cell SegAccount {
    face {
        signal client_deposit(client: String, amount: Int) -> Map
        signal client_withdraw(client: String, amount: Int) -> Map
        signal firm_buffer_in(amount: Int) -> Map
        signal firm_draw(amount: Int) -> Map
        signal seg_status() -> Map
    }

    memory {
        // each client's claim on the seg account — never negative
        claims: Map<String, Int> [persistent, consistent]
        invariant claims >= 0
        // firm money sitting in seg as a buffer: the ONLY pool the firm
        // may draw on. The MF Global wire dies on `excess >= 0`.
        excess: Map<String, Int> [persistent, consistent]
        invariant excess >= 0
        totals: Map<String, Int> [persistent, consistent]
        invariant totals >= 0
    }

    on client_deposit(client: String, amount: Int) {
        if amount <= 0 { return map("ok", false, "reason", "bad amount") }
        claims.set(client, (claims.get(client) ?? 0) + amount)
        totals.set("client", (totals.get("client") ?? 0) + amount)
        return map("ok", true, "claim", claims.get(client))
    }

    on client_withdraw(client: String, amount: Int) {
        if amount <= 0 { return map("ok", false, "reason", "bad amount") }
        let c = claims.get(client) ?? 0
        claims.set(client, c - amount)     // raises past the client's claim
        totals.set("client", (totals.get("client") ?? 0) - amount)
        return map("ok", true, "claim", c - amount)
    }

    on firm_buffer_in(amount: Int) {
        if amount <= 0 { return map("ok", false, "reason", "bad amount") }
        excess.set("buffer", (excess.get("buffer") ?? 0) + amount)
        return map("ok", true, "excess", excess.get("buffer"))
    }

    on firm_draw(amount: Int) {
        if amount <= 0 { return map("ok", false, "reason", "bad amount") }
        let e = excess.get("buffer") ?? 0
        // THE WALL: the firm draws on excess and ONLY excess. There is no
        // code path from here to `claims` or to the client total.
        excess.set("buffer", e - amount)   // raises if it touches client money
        return map("ok", true, "excess", e - amount)
    }

    on seg_status() {
        let client_total = totals.get("client") ?? 0
        let buffer = excess.get("buffer") ?? 0
        return map("client_funds", client_total, "firm_excess", buffer,
                   "seg_account", client_total + buffer)
    }
}

cell test SegTests {
    rules {
        assert client_deposit("acct-101", 50000).ok == true
        assert client_deposit("acct-202", 30000).ok == true
        assert firm_buffer_in(5000).ok == true
        assert seg_status().seg_account == 85000
        // the firm draws within its excess: fine
        assert firm_draw(4000).excess == 1000
        // THE MF GLOBAL WIRE: 10,000 against 1,000 of excess. The other
        // 9,000 would be customer money. The write itself is rejected.
        assert_fails firm_draw(10000)
        assert seg_status().firm_excess == 1000
        assert seg_status().client_funds == 80000
        // and there is no handler that can touch claims for the firm —
        // even a direct rogue write below zero is refused by the slot
        assert_fails excess.set("buffer", -1)
        assert_fails claims.set("acct-101", -1)
        // clients withdraw their own money freely, up to their claim
        assert client_withdraw("acct-202", 30000).claim == 0
        assert_fails client_withdraw("acct-202", 1)
        assert client_withdraw("acct-101", 20000).claim == 30000
        // conservation across the whole story
        assert seg_status().client_funds == 30000
        assert seg_status().seg_account == 31000
    }
}

What the toolchain says

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

$ soma check client_money_segregation.cell
✓ All checks passed.
$ soma test client_money_segregation.cell
test SegTests ...
  ✓ assert client_deposit("acct-101", 50000).ok == true
  ✓ assert client_deposit("acct-202", 30000).ok == true
  ✓ assert firm_buffer_in(5000).ok == true
  ✓ assert seg_status().seg_account == 85000
  ✓ assert firm_draw(4000).excess == 1000
  ✓ assert_fails firm_draw(10000) — raised memory invariant violated on 'excess': excess >= 0 — rejected write of -9000 (key "buffer"); the slot is unchanged
  ✓ assert seg_status().firm_excess == 1000
  ✓ assert seg_status().client_funds == 80000
  ✓ assert_fails excess.set("buffer", -1) — raised memory invariant violated on 'excess': excess >= 0 — rejected write of -1 (key "buffer"); the slot is unchanged
  ✓ assert_fails claims.set("acct-101", -1) — raised memory invariant violated on 'claims': claims >= 0 — rejected write of -1 (key "acct-101"); the slot is unchanged
  ✓ assert client_withdraw("acct-202", 30000).claim == 0
  ✓ assert_fails client_withdraw("acct-202", 1) — raised memory invariant violated on 'claims': claims >= 0 — rejected write of -1 (key "acct-202"); the slot is unchanged
  ✓ assert client_withdraw("acct-101", 20000).claim == 30000
  ✓ assert seg_status().client_funds == 30000
  ✓ assert seg_status().seg_account == 31000

15 tests: 15 passed, 0 failed
$ soma verify client_money_segregation.cell
Verifying client_money_segregation.cell...

SegAccount/invariants:

  ✓ invariant claims >= 0 — writer 'client_deposit' proven by induction (writes (claims.get(client) ?? 0) + amount)
  ⚠ invariant claims >= 0 — runtime-checked (computed values): client_withdraw → claims
  ✓ invariant excess >= 0 — writer 'firm_buffer_in' proven by induction (writes (excess.get("buffer") ?? 0) + amount)
  ⚠ invariant excess >= 0 — runtime-checked (computed values): firm_draw → excess
  ✓ invariant totals >= 0 — writer 'client_deposit' proven by induction (writes (totals.get("client") ?? 0) + amount)
  ⚠ invariant totals >= 0 — runtime-checked (computed values): client_withdraw → totals because `totals` has no known range

3 passed, 3 warnings, 0 failures
Temporal: 0 passed, 0 failed
VERIFY OK

Builtins used

map

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