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