Letter of credit — the bank pays against verified documents, never trust
A documentary letter of credit (UCP 600) exists because exporter and importer don't trust each other across an ocean: the bank promises to pay the seller, but ONLY against documents proving shipment. The classic loss is paying on presented-but-unexamined documents — the Salomon-era trade-finance frauds ran exactly this gap, with bills of lading for cargo that never sailed. Here `honored` is reachable only through `docs_verified`; presenting is not verifying; partial drawings draw down a credit slot that refuses to go below zero, so the beneficiary can never draw more than the credit line. Expired credits are terminal.
A Soma program from the escrow_finance part of the verified corpus: 116 lines, cells LetterOfCredit, LocTests. It passes soma check with soma 3.2.4.
How it is built
The cell LetterOfCredit declares 6 signals in its face: issue(id: String, credit: Int) -> Map, present_docs(id: String) -> Map, verify_docs(id: String, conforming: Bool) -> Map, honor_drawing(id: String, amount: Int) -> Map, expire(id: String) -> Map and undrawn(id: String) -> Int; keeps 1 memory slot: credit (Map<String, Int>, persistent and consistent) under the invariant credit >= 0. Its state machine loc starts in applied and allows 7 transitions: applied -> issued, issued -> docs_presented, issued -> expired, docs_presented -> docs_verified, docs_presented -> discrepant, discrepant -> docs_presented and docs_verified -> honored. expired and honored are terminal. Handlers: issue (line 47), present_docs (line 54), verify_docs (line 59), honor_drawing (line 68), expire (line 79) and undrawn (line 84).
The file also holds 1 test cell (LocTests) with 17 assertions, run by soma test.
The source
letter_of_credit.cell
// ============================================================================
// LETTER OF CREDIT — the bank pays against verified documents, never trust
// ============================================================================
//
// A documentary letter of credit (UCP 600) exists because exporter and
// importer don't trust each other across an ocean: the bank promises to
// pay the seller, but ONLY against documents proving shipment. The classic
// loss is paying on presented-but-unexamined documents — the Salomon-era
// trade-finance frauds ran exactly this gap, with bills of lading for
// cargo that never sailed. Here `honored` is reachable only through
// `docs_verified`; presenting is not verifying; partial drawings draw down
// a credit slot that refuses to go below zero, so the beneficiary can
// never draw more than the credit line. Expired credits are terminal.
// ============================================================================
cell LetterOfCredit {
face {
signal issue(id: String, credit: Int) -> Map
signal present_docs(id: String) -> Map
signal verify_docs(id: String, conforming: Bool) -> Map
signal honor_drawing(id: String, amount: Int) -> Map
signal expire(id: String) -> Map
signal undrawn(id: String) -> Int
}
memory {
// the credit line — drawings reduce it, and it cannot go negative:
// an over-drawing is a rejected write, not an accounting incident
credit: Map<String, Int> [persistent, consistent]
invariant credit >= 0
}
// honored and expired are terminal. The fraud edge — issued (or even
// docs_presented) straight to honored — does not exist: payment
// happens only from docs_verified.
state loc {
initial: applied
applied -> issued
issued -> docs_presented
issued -> expired
docs_presented -> docs_verified
docs_presented -> discrepant
discrepant -> docs_presented
docs_verified -> honored
}
on issue(id: String, credit_amt: Int) {
if credit_amt <= 0 { return map("ok", false, "reason", "bad credit") }
transition(id, "issued")
credit.set(id, credit_amt)
return map("ok", true, "credit", credit_amt)
}
on present_docs(id: String) {
transition(id, "docs_presented")
return map("ok", true, "state", get_status(id))
}
on verify_docs(id: String, conforming: Bool) {
if conforming {
transition(id, "docs_verified")
} else {
transition(id, "discrepant") // seller may cure and re-present
}
return map("ok", true, "state", get_status(id))
}
on honor_drawing(id: String, amount: Int) {
if amount <= 0 { return map("ok", false, "reason", "bad amount") }
if get_status(id) != "docs_verified" {
return map("ok", false, "reason", "documents not verified")
}
let line = credit.get(id) ?? 0
credit.set(id, line - amount) // raises if drawing exceeds credit
transition(id, "honored") // now provably valid
return map("ok", true, "paid", amount, "undrawn", line - amount)
}
on expire(id: String) {
transition(id, "expired") // only an un-presented credit expires
return map("ok", true, "state", get_status(id))
}
on undrawn(id: String) {
return credit.get(id) ?? 0
}
}
cell test LocTests {
rules {
assert issue("loc1", 80000).ok == true
// PAY ON TRUST: honored without documents — the edge does not exist
assert_fails transition("loc1", "honored")
assert present_docs("loc1").ok == true
// presented is NOT verified: still no payment edge
assert_fails transition("loc1", "honored")
// discrepant docs bounce back for cure, then re-present
assert verify_docs("loc1", false).state == "discrepant"
assert_fails transition("loc1", "honored")
assert present_docs("loc1").ok == true
assert verify_docs("loc1", true).state == "docs_verified"
// OVER-DRAWING: 90k against an 80k credit — the slot refuses
assert_fails honor_drawing("loc1", 90000)
assert undrawn("loc1") == 80000
assert honor_drawing("loc1", 80000).paid == 80000
// honored is terminal: documents cannot be "re-presented" for more
assert_fails transition("loc1", "docs_presented")
assert_fails transition("loc1", "honored")
assert_fails credit.set("loc1", -1)
// an untouched credit can expire — and stays expired
assert issue("loc2", 5000).ok == true
assert expire("loc2").ok == true
assert_fails transition("loc2", "docs_presented")
}
}What the toolchain says
$ soma check letter_of_credit.cell
warning: parameter 2 of `issue`: the face calls it `credit`, the handler `credit_amt` — an HTTP JSON body is matched by NAME (the handler's), and `soma describe` publishes the face's: use one name
--> letter_of_credit.cell:18:9
|
18 | signal issue(id: String, credit: Int) -> Map
| ^
✓ 1 warning(s), no errors.$ soma test letter_of_credit.cell
test LocTests ...
✓ assert issue("loc1", 80000).ok == true
✓ assert_fails transition("loc1", "honored") — raised invalid transition: issued → honored. Current state: 'issued'. Valid targets: [docs_presented, expired]
✓ assert present_docs("loc1").ok == true
✓ assert_fails transition("loc1", "honored") — raised invalid transition: docs_presented → honored. Current state: 'docs_presented'. Valid targets: [docs_verified, discrepant]
✓ assert verify_docs("loc1", false).state == "discrepant"
✓ assert_fails transition("loc1", "honored") — raised invalid transition: discrepant → honored. Current state: 'discrepant'. Valid targets: [docs_presented]
✓ assert present_docs("loc1").ok == true
✓ assert verify_docs("loc1", true).state == "docs_verified"
✓ assert_fails honor_drawing("loc1", 90000) — raised memory invariant violated on 'credit': credit >= 0 — rejected write of -10000 (key "loc1"); the slot is unchanged
✓ assert undrawn("loc1") == 80000
✓ assert honor_drawing("loc1", 80000).paid == 80000
✓ assert_fails transition("loc1", "docs_presented") — raised invalid transition: honored → docs_presented. Current state: 'honored'. Valid targets: []
✓ assert_fails transition("loc1", "honored") — raised invalid transition: honored → honored. Current state: 'honored'. Valid targets: []
✓ assert_fails credit.set("loc1", -1) — raised memory invariant violated on 'credit': credit >= 0 — rejected write of -1 (key "loc1"); the slot is unchanged
✓ assert issue("loc2", 5000).ok == true
✓ assert expire("loc2").ok == true
✓ assert_fails transition("loc2", "docs_presented") — raised invalid transition: expired → docs_presented. Current state: 'expired'. Valid targets: []
17 tests: 17 passed, 0 failed$ soma verify letter_of_credit.cell
Verifying letter_of_credit.cell...
State machine 'loc': 7 states, initial 'applied'
States: [applied, discrepant, docs_presented, docs_verified, expired, honored, issued]
✓ 7 states, 7 transitions
✓ all states reachable from 'applied'
✓ terminal states: [expired, honored]
✓ no deadlocks
✓ liveness: every state can eventually reach a terminal state
✓ think-isolated: CTL safety properties hold regardless of LLM output (5 handlers, 6 literal transitions, 0 dynamic)
✓ termination: all 6 handlers structurally terminate
✓ refinement: handler `issue` ⟶ {issued}
✓ refinement: handler `present_docs` ⟶ {docs_presented}
✓ refinement: handler `verify_docs` ⟶ {docs_verified [if conforming], discrepant [if not (conforming)]}
✓ refinement: handler `honor_drawing` ⟶ {docs_verified → honored}
✓ refinement: handler `expire` ⟶ {expired}
· transition sources: `issue`, `present_docs`, `verify_docs`, `expire` 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)
LetterOfCredit/invariants:
✓ invariant credit >= 0 — writer 'issue' proven by induction (writes credit_amt)
⚠ invariant credit >= 0 — runtime-checked (computed values): honor_drawing → credit
13 passed, 1 warnings, 0 failures
Temporal properties for 'loc':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK