soma.cell

Campaign finance — the contribution limit lives in the ledger, not the law

In 2014 Dinesh D'Souza was convicted for funneling money past the federal per-donor limit through straw donors he reimbursed. Straw schemes work because the limit is checked at filing time, months after the money moved. Here the per-donor cap ($2,900, the 2022-cycle federal limit) is a memory invariant: the cumulative-total write for any donor name above the cap is refused at the instant of donation. The reporting lifecycle is one-way — once the committee files, `reported` is terminal: soma verify proves the books cannot be re-opened to massage totals.

A Soma program from the civic part of the verified corpus: 106 lines, cells Committee, CommitteeTests. It passes soma check with soma 3.2.4.

How it is built

The cell Committee declares 7 signals in its face: open_books() -> Map, contribute(donor: String, amount: Int) -> Map, close_books() -> Map, file_report() -> Map, donor_total(donor: String) -> Int, raised_total() -> Int and phase() -> String; keeps 1 memory slot: donated (Map<String, Int>, persistent and consistent) under the invariant donated >= 0 && donated <= 2900. Its state machine cycle starts in registered and allows 3 transitions: registered -> raising, raising -> closed and closed -> reported. reported is terminal. Handlers: phase (line 39), open_books (line 41), contribute (line 47), close_books (line 63), file_report (line 69), donor_total (line 75) and raised_total (line 77).

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

Generated from soma describe on this file.

The source

campaign_finance.cell

// ============================================================================
// CAMPAIGN FINANCE — the contribution limit lives in the ledger, not the law
// ============================================================================
// In 2014 Dinesh D'Souza was convicted for funneling money past the federal
// per-donor limit through straw donors he reimbursed. Straw schemes work
// because the limit is checked at filing time, months after the money moved.
// Here the per-donor cap ($2,900, the 2022-cycle federal limit) is a memory
// invariant: the cumulative-total write for any donor name above the cap is
// refused at the instant of donation. The reporting lifecycle is one-way —
// once the committee files, `reported` is terminal: soma verify proves the
// books cannot be re-opened to massage totals.
// ============================================================================

cell Committee {
    face {
        signal open_books() -> Map
        signal contribute(donor: String, amount: Int) -> Map
        signal close_books() -> Map
        signal file_report() -> Map
        signal donor_total(donor: String) -> Int
        signal raised_total() -> Int
        signal phase() -> String
    }

    memory {
        // donor -> cumulative dollars this cycle. $2,901 is unrepresentable.
        donated: Map<String, Int> [persistent, consistent]
        invariant donated >= 0 && donated <= 2900
    }

    // reported is terminal: filed books are filed forever.
    state cycle {
        initial: registered
        registered -> raising
        raising -> closed
        closed -> reported
    }

    on phase() { return get_status("cycle") }

    on open_books() {
        let r = try { transition("cycle", "raising") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true)
    }

    on contribute(donor: String, amount: Int) {
        if phase() != "raising" {
            return map("ok", false, "reason", "books are {phase()}")
        }
        if amount <= 0 { return map("ok", false, "reason", "amount must be positive") }
        let sofar = donated.get(donor) ?? 0
        // THE WALL: past $2,900 the ledger refuses, at donation time,
        // not at filing time.
        let w = try { donated.set(donor, sofar + amount) }
        if w.error != () {
            return map("ok", false, "reason",
                "limit: {donor} at {sofar}, +{amount} exceeds 2900")
        }
        return map("ok", true, "donor", donor, "total", sofar + amount)
    }

    on close_books() {
        let r = try { transition("cycle", "closed") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "raised", raised_total())
    }

    on file_report() {
        let r = try { transition("cycle", "reported") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "raised", raised_total())
    }

    on donor_total(donor: String) { return donated.get(donor) ?? 0 }

    on raised_total() {
        let names = donated.keys()
        let n = 0
        for d in names { n = n + (donated.get(d) ?? 0) }
        return n
    }
}

cell test CommitteeTests {
    rules {
        assert contribute("ada", 100).ok == false         // books not open yet
        assert open_books().ok == true
        assert contribute("ada", 2000).ok == true
        assert contribute("ada", 1000).ok == false        // 3000 > 2900: refused
        assert donor_total("ada") == 2000                 // ledger untouched
        assert contribute("ada", 900).ok == true          // exactly the cap: fine
        assert donor_total("ada") == 2900
        assert contribute("ada", 1).ok == false           // one dollar over: never
        assert_fails donated.set("ada", 2901)               // unrepresentable
        assert_fails donated.set("bob", -10)                // no laundering negatives
        assert contribute("bob", 500).ok == true
        assert raised_total() == 3400
        assert close_books().ok == true
        assert contribute("late", 5).ok == false          // books are closed
        assert file_report().ok == true
        assert_fails transition("cycle", "raising")       // filed is forever
        assert_fails transition("cycle", "closed")
    }
}

What the toolchain says

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

$ soma check campaign_finance.cell
✓ All checks passed.
$ soma test campaign_finance.cell
test CommitteeTests ...
  ✓ assert contribute("ada", 100).ok == false
  ✓ assert open_books().ok == true
  ✓ assert contribute("ada", 2000).ok == true
  ✓ assert contribute("ada", 1000).ok == false
  ✓ assert donor_total("ada") == 2000
  ✓ assert contribute("ada", 900).ok == true
  ✓ assert donor_total("ada") == 2900
  ✓ assert contribute("ada", 1).ok == false
  ✓ assert_fails donated.set("ada", 2901) — raised memory invariant violated on 'donated': donated >= 0 && donated <= 2900 — rejected write of 2901 (key "ada"); the slot is unchanged
  ✓ assert_fails donated.set("bob", -10) — raised memory invariant violated on 'donated': donated >= 0 && donated <= 2900 — rejected write of -10 (key "bob"); the slot is unchanged
  ✓ assert contribute("bob", 500).ok == true
  ✓ assert raised_total() == 3400
  ✓ assert close_books().ok == true
  ✓ assert contribute("late", 5).ok == false
  ✓ assert file_report().ok == true
  ✓ assert_fails transition("cycle", "raising") — raised invalid transition: reported → raising. Current state: 'reported'. Valid targets: []
  ✓ assert_fails transition("cycle", "closed") — raised invalid transition: reported → closed. Current state: 'reported'. Valid targets: []

17 tests: 17 passed, 0 failed
$ soma verify campaign_finance.cell
Verifying campaign_finance.cell...

State machine 'cycle': 4 states, initial 'registered'
  States: [closed, raising, registered, reported]

  ✓ 4 states, 3 transitions
  ✓ all states reachable from 'registered'
  ✓ terminal states: [reported]
  ✓ 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 7 handlers structurally terminate
  ✓ refinement: handler `open_books` ⟶ {raising}
  ✓ refinement: handler `close_books` ⟶ {closed}
  ✓ refinement: handler `file_report` ⟶ {reported}
  · transition sources: `open_books`, `close_books`, `file_report` 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)

Committee/invariants:

  ✓ invariant donated >= 0 — writer 'contribute' proven by induction (writes sofar + amount)
  ⚠ invariant donated >= 0 && donated <= 2900 — runtime-checked (computed values): contribute → donated [donated <= 2900] because `sofar + amount` is only known to lie in [1, ∞) — narrow it: `require (sofar + amount) <= 2900 else …`

11 passed, 1 warnings, 0 failures

  Temporal properties for 'cycle':
  ✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK

Builtins used

get_status, map, transition

Source file: /corpus/civic/campaign_finance.cell · examples/corpus/civic/campaign_finance.cell