soma.cell

Dividend distribution — payments can never exceed the declared pool

When a board declares a dividend, that number is legally binding — and transfer agents have paid out MORE than the declaration through duplicate shareholder records or a re-run batch job (the same disease as the 2021 "accidental" duplicate ACH dividend runs brokers had to claw back). Structure: the declared pool is a slot every payment decrements, with `pool >= 0` as the invariant — the payment that would overspend the declaration is a rejected write. A shareholder can be paid once per declaration, and a closed distribution is terminal.

A Soma program from the escrow_finance part of the verified corpus: 96 lines, cells Dividend, DividendTests. It passes soma check with soma 3.2.4.

How it is built

The cell Dividend declares 4 signals in its face: declare(id: String, total: Int) -> Map, pay_holder(id: String, holder: String, amount: Int) -> Map, close_books(id: String) -> Map and pool_left(id: String) -> Int; keeps 2 memory slots: pool (Map<String, Int>, persistent and consistent) under the invariant pool >= 0 and paid (Map<String, Int>, persistent and consistent) under the invariant paid >= 0. Its state machine distribution starts in proposed and allows 3 transitions: proposed -> declared, declared -> paying and paying -> closed. closed is terminal. Handlers: declare (line 40), pay_holder (line 48), close_books (line 64) and pool_left (line 69).

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

Generated from soma describe on this file.

The source

dividend_pool.cell

// ============================================================================
// DIVIDEND DISTRIBUTION — payments can never exceed the declared pool
// ============================================================================
//
// When a board declares a dividend, that number is legally binding — and
// transfer agents have paid out MORE than the declaration through duplicate
// shareholder records or a re-run batch job (the same disease as the 2021
// "accidental" duplicate ACH dividend runs brokers had to claw back).
// Structure: the declared pool is a slot every payment decrements, with
// `pool >= 0` as the invariant — the payment that would overspend the
// declaration is a rejected write. A shareholder can be paid once per
// declaration, and a closed distribution is terminal.
// ============================================================================

cell Dividend {
    face {
        signal declare(id: String, total: Int) -> Map
        signal pay_holder(id: String, holder: String, amount: Int) -> Map
        signal close_books(id: String) -> Map
        signal pool_left(id: String) -> Int
    }

    memory {
        // the declared pool — strictly drawn down, never overdrawn
        pool: Map<String, Int> [persistent, consistent]
        invariant pool >= 0
        paid: Map<String, Int> [persistent, consistent]
        invariant paid >= 0
    }

    // distribution lifecycle; closed is terminal — the batch job that
    // re-runs against a closed declaration finds no state to act in.
    state distribution {
        initial: proposed
        proposed -> declared
        declared -> paying
        paying -> closed
    }

    on declare(id: String, total: Int) {
        if total <= 0 { return map("ok", false, "reason", "bad total") }
        transition(id, "declared")
        transition(id, "paying")
        pool.set(id, total)
        return map("ok", true, "declared", total)
    }

    on pay_holder(id: String, holder: String, amount: Int) {
        if get_status(id) != "paying" {
            return map("ok", false, "reason", "distribution is not paying")
        }
        if amount <= 0 { return map("ok", false, "reason", "bad amount") }
        let k = "{id}:{holder}"
        if (paid.get(k) ?? 0) > 0 {
            // THE DUPLICATE RECORD: same holder, same declaration — once
            return map("ok", false, "reason", "{holder} already paid")
        }
        let left = pool.get(id) ?? 0
        pool.set(id, left - amount)        // raises past the declaration
        paid.set(k, amount)
        return map("ok", true, "holder", holder, "left", left - amount)
    }

    on close_books(id: String) {
        transition(id, "closed")
        return map("ok", true, "unclaimed", pool.get(id) ?? 0)
    }

    on pool_left(id: String) {
        return pool.get(id) ?? 0
    }
}

cell test DividendTests {
    rules {
        assert declare("q2", 10000).ok == true
        assert pay_holder("q2", "fund-a", 6000).ok == true
        assert pay_holder("q2", "fund-b", 3000).ok == true
        // THE RE-RUN BATCH: fund-a appears again — refused
        assert pay_holder("q2", "fund-a", 6000).ok == false
        // THE OVERSPEND: 1,500 against the 1,000 left — the write raises
        assert_fails pay_holder("q2", "fund-c", 1500)
        assert pool_left("q2") == 1000
        assert pay_holder("q2", "fund-c", 1000).ok == true
        assert pool_left("q2") == 0
        // nothing left: even one more cent is an impossible write
        assert_fails pay_holder("q2", "fund-d", 1)
        assert_fails pool.set("q2", -1)
        assert close_books("q2").unclaimed == 0
        // closed is terminal — the distribution cannot re-open and re-pay
        assert pay_holder("q2", "fund-e", 1).ok == false
        assert_fails transition("q2", "paying")
        assert_fails transition("q2", "declared")
    }
}

What the toolchain says

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

$ soma check dividend_pool.cell
✓ All checks passed.
$ soma test dividend_pool.cell
test DividendTests ...
  ✓ assert declare("q2", 10000).ok == true
  ✓ assert pay_holder("q2", "fund-a", 6000).ok == true
  ✓ assert pay_holder("q2", "fund-b", 3000).ok == true
  ✓ assert pay_holder("q2", "fund-a", 6000).ok == false
  ✓ assert_fails pay_holder("q2", "fund-c", 1500) — raised memory invariant violated on 'pool': pool >= 0 — rejected write of -500 (key "q2"); the slot is unchanged
  ✓ assert pool_left("q2") == 1000
  ✓ assert pay_holder("q2", "fund-c", 1000).ok == true
  ✓ assert pool_left("q2") == 0
  ✓ assert_fails pay_holder("q2", "fund-d", 1) — raised memory invariant violated on 'pool': pool >= 0 — rejected write of -1 (key "q2"); the slot is unchanged
  ✓ assert_fails pool.set("q2", -1) — raised memory invariant violated on 'pool': pool >= 0 — rejected write of -1 (key "q2"); the slot is unchanged
  ✓ assert close_books("q2").unclaimed == 0
  ✓ assert pay_holder("q2", "fund-e", 1).ok == false
  ✓ assert_fails transition("q2", "paying") — raised invalid transition: closed → paying. Current state: 'closed'. Valid targets: []
  ✓ assert_fails transition("q2", "declared") — raised invalid transition: closed → declared. Current state: 'closed'. Valid targets: []

14 tests: 14 passed, 0 failed
$ soma verify dividend_pool.cell
Verifying dividend_pool.cell...

State machine 'distribution': 4 states, initial 'proposed'
  States: [closed, declared, paying, proposed]

  ✓ 4 states, 3 transitions
  ✓ all states reachable from 'proposed'
  ✓ terminal states: [closed]
  ✓ no deadlocks
  ✓ liveness: every state can eventually reach a terminal state
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (2 handlers, 3 literal transitions, 0 dynamic)
  ✓ termination: all 4 handlers structurally terminate
  ✓ refinement: handler `declare` ⟶ {declared, declared → paying}
  ✓ refinement: handler `close_books` ⟶ {closed}
  · transition sources: `declare`, `close_books` 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)

Dividend/invariants:

  ✓ invariant pool >= 0 — writer 'declare' proven by induction (writes total)
  ⚠ invariant pool >= 0 — runtime-checked (computed values): pay_holder → pool
  ✓ invariant paid >= 0 — writer 'pay_holder' proven by induction (writes amount)

11 passed, 1 warnings, 0 failures

  Temporal properties for 'distribution':
  ✓ 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/dividend_pool.cell · examples/corpus/escrow_finance/dividend_pool.cell