Safe Owner List Invariants
The owners mapping forms a proper loop-free circular linked list.
The Safe smart account manages its owners using a singly-linked list stored in a mapping owners: address → address. The first node of the list is called SENTINEL and anchors the list at address key 0x1.
The list is updated by four operations: setupOwners builds it from an address array, addOwner inserts at the head, removeOwner unlinks a node, and swapOwner replaces one owner with another in a single step.
Why It Matters
Safe is the most widely used multi-signature wallet on Ethereum, securing billions of dollars. If the owners list in OwnerManager were malformed, approval checks could skip a real owner or treat the signer set differently than what is expected.
How This Was Modeled and Proven
The ownership operations are modeled in Verity from the Solidity OwnerManager.sol implementation in safe-smart-account; the Verity contract slice is in Contract.lean. Each function mutates the linked list differently:
- setupOwners: constructs the initial linked list from a list of addresses (base case)
- addOwner: head insertion. The new owner is placed between SENTINEL and the old head
- removeOwner: chain excision. The previous owner's pointer is redirected past the removed node
- swapOwner: atomic replacement. The old owner is unlinked and the new owner spliced into its position
How do you prove an owner is in the list? You show a concrete path: start at SENTINEL, follow the owners mapping one hop at a time, and arrive at that owner. If such a path exists, the owner is reachable. If no path exists, the owner is not in the list.
Beyond reachability, each function must preserve the full structural invariant: every node has exactly one incoming edge (unique predecessor), the list has no internal cycles, and isOwner changes exactly the intended addresses. The unique predecessor property is what makes removeOwner and swapOwner work: if you re-route a pointer, you need to know that nothing else was pointing to the old target. Because each node has exactly one incoming edge, that guarantee holds. An earlier draft assumed antisymmetry of reachability (matching Certora's spec), but that property is false on circular lists. Unique predecessor captures the same “no branching” truth without breaking on the cycle.
Scope
The owners mapping forms a proper loop-free circular linked list.
In Lean, that goal is split into four families of properties:
ownerListInvariant: membership (non-zero successor) is equivalent to reachability from SENTINELuniquePredecessor: each non-zero node has at most one non-zero predecessor (the list is a simple path with no branching)acyclic: the linked list has no internal SENTINEL cyclesisOwner correctness: each operation changes exactly the intended owners and leaves all others unchanged
The invariant properties correspond to Certora's OwnerReach.spec. The functional correctness proofs go beyond what Certora specifies. Threshold management is elided as it does not affect the owners mapping.
Proof artifacts
- Contract.lean contains the Verity contract slice.
- Specs.lean defines the specifications.
- All 16 theorems are proven. Proofs.lean is
sorry-free. - All proofs are verified by Lean 4's kernel. If any step were wrong, the code would not compile.
| Function | Theorem | Status |
|---|---|---|
| setupOwners | setupOwners_uniquePredecessor | Proven |
| setupOwners | setupOwners_ownerListInvariant | Proven |
| setupOwners | setupOwners_acyclicity | Proven |
| setupOwners | setupOwners_isOwnerCorrectness | Proven |
| addOwner | addOwner_uniquePredecessor | Proven |
| addOwner | addOwner_ownerListInvariant | Proven |
| addOwner | addOwner_acyclicity | Proven |
| addOwner | addOwner_isOwnerCorrectness | Proven |
| removeOwner | removeOwner_uniquePredecessor | Proven |
| removeOwner | removeOwner_ownerListInvariant | Proven |
| removeOwner | removeOwner_acyclicity | Proven |
| removeOwner | removeOwner_isOwnerCorrectness | Proven |
| swapOwner | swapOwner_uniquePredecessor | Proven |
| swapOwner | swapOwner_ownerListInvariant | Proven |
| swapOwner | swapOwner_acyclicity | Proven |
| swapOwner | swapOwner_isOwnerCorrectness | Proven |
Verify it yourself
git clone https://github.com/lfglabs-dev/verity-benchmark cd verity-benchmark lake build Benchmark.Cases.Safe.OwnerManagerReach.Compile
If the build succeeds, the proofs are correct. Source repository
Hypotheses
The proofs use zero axioms. The only assumptions are the Solidity require guards the contract already enforces, and a three-field inductive invariant that is proven preserved by every mutation. Properties like noSelfLoops, freshInList, and acyclic are derived inside the proofs, not assumed.
inputs pass the contract's require checksOwnerManager.sol (GS203–GS205)
Every ownership-mutating function begins with
requirechecks on its arguments, drawn fromowner != 0,owner != SENTINEL,owners[owner] == 0, andowners[prevOwner] == owner. The proofs consume these in exactly the form the contract enforces, so no additional trust is needed.owner list is well formed before each callProven preserved by every mutation
Before a function executes, the bundled invariant
SafeOwnerInvariantholds. Its three fields areownerListInvariant(membership equals reachability),uniquePredecessor(no branching), andzeroInert(the zero address is inert). The proof is inductive:setupOwnersestablishes it from clean storage (base case);addOwner,removeOwner, andswapOwnereach preserve it (inductive step). Because preservation is proven, the invariant is a theorem, not a blind assumption.
Learn More
Certora OwnerReach.spec, the original specification this work is based on.
What is a formal proof? A short explanation for non-specialists.
More research
Kuru V2 Shared Withdrawal Budget
A successful batch of ordinary withdrawals in the model reduces the shared budget by exactly its recorded payout value, across all accounts and tokens.
Dromos MetaDEX Allocation Conservation
In the root-ledger model, chain allocations sum to committed voting power.

KPK Shares Settlement Accounting
A successful kpkShares settlement batch processes each eligible request at most once and accounts exactly for its fees, shares and asset transfers.