Wells Fargo wrong-account closures — 2017
Cost: ~545 customer accounts wrongly closed; settlement and remediation costs across a multi-year regulatory tail · Time-to-detect: months (closures executed in batches; complaints accumulated before the pattern surfaced) · Root cause class: T3 (referential integrity — unbounded join cardinality on a CSV-sourced key)
What happened
In the course of an internal back-office cleanup at Wells Fargo around 2017, a customer-service representative loaded a CSV of account identifiers into a working table and joined it against the live customer-account ledger to drive a batch of account closures. The CSV had been hand-assembled from upstream sources whose account-id formats had drifted over the years — some rows carried the 10-digit account number, some carried an internal customer-relationship key, and a portion of the rows were ambiguous because the same numeric value could resolve to either of two different accounts depending on which keyspace the downstream join used. The closure job joined on the raw value with no key-space discriminator, the join was many-to-one in some rows and one-to-many in others, and a few hundred accounts belonging to customers who were never the intended target were marked for closure. The error was not detected at execution time; it surfaced over the following weeks as customers discovered their accounts had been frozen or closed and escalated through the contact center.
The pattern
A join key was sourced from a CSV with no enforced schema and joined into the live ledger on raw value alone. The verifier-relevant property here is a JOIN whose cardinality the planner cannot bound from declared keys: the CSV column carried no unique constraint, no key-space discriminator, and no foreign-key declaration to either of the two candidate parent tables. At plan time, the join's output row count is unbounded in the size of the ambiguous-row population. The same shape recurs whenever an operational batch is driven by a hand-curated key list joined into a regulated table — customer-merge jobs, account-migration scripts, fraud-flag backfills, GDPR-deletion queues.
How veric would catch it
veric's T3 referential-integrity tier walks every JOIN in the model graph and proves an upper-bound row count from the declared keys on each side. A join against a CSV-sourced staging table whose key column has no unique test and no foreign-key declaration to a single parent fails the bound check: "join closure_batch ⨝ customer_accounts on account_id is unbounded; left side declares no unique key, right side has two candidate parent tables (accounts.account_number and customer_relationships.crm_id) — T3 referential-integrity VIOLATED, key-space ambiguous." In the PR diff that introduced the closure script, this is a deploy-time failure rather than a months-long postmortem.
Try it: open the example below and watch the verdict change as you toggle the offending pattern on and off.
See also
- /explore — the witness — when the join-cardinality bound fails, the verifier hands back the ambiguous CSV row that triggered it.
- /explore — the lattice — referential-integrity bounds are abstract values that join across the model graph.
- Adjacent incidents: Knight Capital 2012, marketplace refund double-count.
Sources
- Reporting on Wells Fargo's account-closure and back-office practices in 2017 was extensive across the New York Times, Wall Street Journal, and Reuters during the broader fake-accounts and operational-practices coverage of that period; the wrong-account-closure pattern is documented in customer complaints filed with the Consumer Financial Protection Bureau and in the bank's subsequent consent orders. Specifics of the CSV-driven batch shape here are reconstructed from the public pattern and used illustratively to anchor the T3 join-cardinality bug class.