Files
zeekayandHanzo Dev 638fba4c90 Publish under the org, not a person
Bylines, paper authorship, correspondence addresses and the founder blurb under
each post named an individual. They name the organisation that published the
work instead: the brand on posts and pages, the research byline on papers, and
research@ in place of a personal address. Both are what these repositories
already used elsewhere — nothing new was coined.

Co-authors are untouched. Where a paper named someone else alongside, they stay
named; only the one byline changed. Bibliography entries, running heads,
PDF metadata and affiliations move with it, so a citation still resolves to
whoever the cited paper names.

Sample and fixture data uses the placeholder name these tests already use
elsewhere, so no assertion changed meaning.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 08:58:14 -07:00

345 lines
14 KiB
Lean4
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/-
Authors: Antje Worring, Zoo Labs Foundation Research
Decentralized Science Organization (DSO) Governance
Formal proofs about voting safety in Zoo's decentralized governance
model. The DSO uses stake-weighted voting with Byzantine-robust
median aggregation for proposal resolution.
Maps to:
- zoo/papers/zoo-dso.tex: DSO specification with Byzantine-robust aggregation
- zoo/ZIPs/: governance proposals at zips.zoo.ngo
Key results:
- No double voting: each member votes at most once per proposal
- Quorum enforced: proposals only pass with sufficient participation
- Conflicting proposals are mutually exclusive
- Delegation preserves total voting weight
Author: Zoo Labs Foundation Research
-/
import Mathlib.Data.Nat.Defs
import Mathlib.Data.Finset.Basic
import Mathlib.Tactic
namespace Governance.DSO
/-! ## Core Structures -/
/-- A member of the DSO with voting weight derived from stake and quality score. -/
structure Member where
id : Nat
/-- Voting weight (stake * quality score) -/
weight : Nat
deriving DecidableEq, Repr, Hashable
/-- Proposal state. -/
structure Proposal where
id : Nat
/-- Set of member IDs who have voted for -/
votersFor : Finset Nat
/-- Set of member IDs who have voted against -/
votersAgainst : Finset Nat
/-- Total weight of for-votes -/
weightFor : Nat
/-- Total weight of against-votes -/
weightAgainst : Nat
/-- Required quorum (minimum total participation weight) -/
quorum : Nat
/-- Required approval threshold (basis points, e.g., 6000 = 60%) -/
threshold : Nat
/-- Optional conflict group: proposals with same group cannot both pass -/
conflictGroup : Option Nat
deriving DecidableEq
/-- A delegation record: delegator gives their weight to delegatee. -/
structure Delegation where
delegator : Nat
delegatee : Nat
weight : Nat
deriving DecidableEq, Repr
/-! ## Voting Operations -/
/-- Check if a member has already voted on a proposal. -/
def hasVoted (p : Proposal) (memberId : Nat) : Bool :=
memberId ∈ p.votersFor || memberId ∈ p.votersAgainst
/-- Cast a vote. Returns none if the member has already voted. -/
def castVote (p : Proposal) (memberId : Nat) (weight : Nat) (support : Bool) :
Option Proposal :=
if hasVoted p memberId then
none -- already voted
else if support then
some { p with
votersFor := p.votersFor ∪ {memberId}
weightFor := p.weightFor + weight }
else
some { p with
votersAgainst := p.votersAgainst ∪ {memberId}
weightAgainst := p.weightAgainst + weight }
/-- Check if a proposal passes: quorum met AND approval threshold met. -/
def passes (p : Proposal) : Bool :=
let totalWeight := p.weightFor + p.weightAgainst
totalWeight ≥ p.quorum && p.weightFor * 10000 ≥ p.threshold * totalWeight
/-! ## Theorem 1: No Double Vote -/
/-- No double voting: once a member has voted, attempting to vote again
returns none. This is enforced by checking the voter sets before
recording the vote. -/
theorem no_double_vote (p : Proposal) (memberId : Nat) (weight : Nat) (support : Bool)
(h : hasVoted p memberId = true) :
castVote p memberId weight support = none := by
simp [castVote, h]
/-- After voting, the member is recorded as having voted. -/
theorem vote_recorded_for (p : Proposal) (memberId : Nat) (weight : Nat)
(h : hasVoted p memberId = false) :
∃ p', castVote p memberId weight true = some p' ∧
memberId ∈ p'.votersFor := by
refine ⟨{ p with votersFor := p.votersFor ∪ {memberId}, weightFor := p.weightFor + weight }, ?_, ?_⟩
· simp [castVote, h]
· simp [Finset.mem_union, Finset.mem_singleton]
/-- After voting against, the member is recorded. -/
theorem vote_recorded_against (p : Proposal) (memberId : Nat) (weight : Nat)
(h : hasVoted p memberId = false) :
∃ p', castVote p memberId weight false = some p' ∧
memberId ∈ p'.votersAgainst := by
refine ⟨{ p with votersAgainst := p.votersAgainst ∪ {memberId}, weightAgainst := p.weightAgainst + weight }, ?_, ?_⟩
· simp [castVote, h]
· simp [Finset.mem_union, Finset.mem_singleton]
/-! ## Theorem 2: Quorum Enforced -/
/-- A proposal with zero participation does not pass (quorum > 0). -/
theorem quorum_enforced (p : Proposal) (h_quorum_pos : p.quorum > 0)
(h_no_votes : p.weightFor = 0 ∧ p.weightAgainst = 0) :
passes p = false := by
simp [passes, h_no_votes.1, h_no_votes.2]
omega
/-- If participation weight is below quorum, the proposal fails
regardless of the vote split. -/
theorem below_quorum_fails (p : Proposal)
(h : p.weightFor + p.weightAgainst < p.quorum) :
passes p = false := by
simp [passes]
omega
/-! ## Theorem 3: Conflicting Proposals Exclusive -/
/-- Two proposals conflict if they share the same conflict group. -/
def conflicts (p₁ p₂ : Proposal) : Prop :=
∃ g, p₁.conflictGroup = some g ∧ p₂.conflictGroup = some g
/-- If two proposals conflict and both have quorum, they cannot both pass
when the total voting weight is bounded (each weight is used at most once).
This models the DSO constraint: a member who votes for proposal A
in a conflict group cannot also vote for proposal B in the same group.
Therefore the total for-weight across conflicting proposals is bounded
by the total available weight. -/
theorem conflicting_proposals_exclusive (p₁ p₂ : Proposal)
(totalWeight : Nat)
(h_pos : totalWeight > 0)
(_h_conflict : conflicts p₁ p₂)
(_h_disjoint_voters : p₁.votersFor ∩ p₂.votersFor = ∅)
(h_weight_bound : p₁.weightFor + p₂.weightFor ≤ totalWeight)
(h_threshold : p₁.threshold ≥ 5001 ∧ p₂.threshold ≥ 5001)
(h_same_quorum : p₁.quorum = totalWeight ∧ p₂.quorum = totalWeight) :
¬(passes p₁ = true ∧ passes p₂ = true) := by
intro ⟨hp₁, hp₂⟩
simp only [passes, Bool.and_eq_true, decide_eq_true_eq, ge_iff_le] at hp₁ hp₂
obtain ⟨hq₁, ha₁⟩ := hp₁
obtain ⟨hq₂, ha₂⟩ := hp₂
rw [h_same_quorum.1] at hq₁
rw [h_same_quorum.2] at hq₂
-- Each passing proposal needs weightFor·10000 ≥ threshold·participation ≥ 5001·totalWeight,
-- since threshold ≥ 5001 and participation ≥ totalWeight (quorum).
have e1 : (5001 : Nat) * totalWeight ≤ p₁.weightFor * 10000 :=
le_trans (Nat.mul_le_mul h_threshold.1 hq₁) ha₁
have e2 : (5001 : Nat) * totalWeight ≤ p₂.weightFor * 10000 :=
le_trans (Nat.mul_le_mul h_threshold.2 hq₂) ha₂
-- But weightFor₁ + weightFor₂ ≤ totalWeight, so their combined for-weight is too small.
have hsum : (p₁.weightFor + p₂.weightFor) * 10000 ≤ totalWeight * 10000 :=
Nat.mul_le_mul_right _ h_weight_bound
nlinarith [e1, e2, hsum, h_pos]
/-! ## Theorem 4: Delegation Weight Preservation -/
/-- Total weight in the system before delegation. -/
def totalWeight (members : List Member) : Nat :=
members.foldl (fun acc m => acc + m.weight) 0
/-- Apply a delegation: transfer weight from delegator to delegatee.
Returns the updated member list. -/
def applyDelegation (members : List Member) (d : Delegation) : List Member :=
members.map fun m =>
if m.id = d.delegator then { m with weight := m.weight - min d.weight m.weight }
else if m.id = d.delegatee then { m with weight := m.weight + d.weight }
else m
/-- The per-member weight transform underlying `applyDelegation`. -/
def tf (d : Delegation) (m : Member) : Member :=
if m.id = d.delegator then { m with weight := m.weight - min d.weight m.weight }
else if m.id = d.delegatee then { m with weight := m.weight + d.weight }
else m
/-- Weight removed from a member (nonzero only for the delegator). -/
def lossOf (d : Delegation) (m : Member) : Nat :=
if m.id = d.delegator then min d.weight m.weight else 0
/-- Weight added to a member (nonzero only for the delegatee). -/
def gainOf (d : Delegation) (m : Member) : Nat :=
if m.id = d.delegator then 0 else if m.id = d.delegatee then d.weight else 0
/-- The accumulator of a weight-fold can be pulled out of the start position. -/
theorem foldl_weight_start (l : List Member) (s : Nat) :
l.foldl (fun acc m => acc + m.weight) s = s + l.foldl (fun acc m => acc + m.weight) 0 := by
induction l generalizing s with
| nil => simp
| cons h2 t2 ih2 =>
rw [List.foldl_cons, List.foldl_cons, ih2 (s + h2.weight), ih2 (0 + h2.weight)]; omega
/-- `totalWeight` equals the plain sum of the member weights. -/
theorem totalWeight_eq_sum (members : List Member) :
totalWeight members = (members.map Member.weight).sum := by
unfold totalWeight
induction members with
| nil => simp
| cons hd tl ih =>
rw [List.foldl_cons, List.map_cons, List.sum_cons, foldl_weight_start tl (0 + hd.weight), ih]
omega
/-- Per-member ledger identity: transformed weight plus loss equals
original weight plus gain (holds unconditionally in `Nat`). -/
theorem elem_balance (d : Delegation) (m : Member) :
(tf d m).weight + lossOf d m = m.weight + gainOf d m := by
unfold tf lossOf gainOf
by_cases h1 : m.id = d.delegator
· rw [if_pos h1, if_pos h1, if_pos h1]
show (m.weight - min d.weight m.weight) + min d.weight m.weight = m.weight + 0
have : min d.weight m.weight ≤ m.weight := Nat.min_le_right _ _
omega
· rw [if_neg h1, if_neg h1, if_neg h1]
by_cases h2 : m.id = d.delegatee
· rw [if_pos h2, if_pos h2]; rfl
· rw [if_neg h2, if_neg h2]
/-- Summed ledger identity over the whole member list. -/
theorem sum_balance (d : Delegation) (l : List Member) :
(l.map (fun m => (tf d m).weight)).sum + (l.map (lossOf d)).sum
= (l.map Member.weight).sum + (l.map (gainOf d)).sum := by
induction l with
| nil => simp
| cons hd tl ih =>
simp only [List.map_cons, List.sum_cons]
have := elem_balance d hd
omega
/-- With distinct ids and the delegator present holding enough weight,
the total weight removed equals exactly `d.weight`. -/
theorem loss_sum (d : Delegation) (l : List Member)
(h_nodup : l.Pairwise (fun a b => a.id ≠ b.id))
(hmem : ∃ m ∈ l, m.id = d.delegator ∧ m.weight ≥ d.weight) :
(l.map (lossOf d)).sum = d.weight := by
induction l with
| nil => simp at hmem
| cons hd tl ih =>
rw [List.pairwise_cons] at h_nodup
obtain ⟨hd_notin, h_tl_nodup⟩ := h_nodup
simp only [List.map_cons, List.sum_cons, lossOf]
by_cases h1 : hd.id = d.delegator
· rw [if_pos h1]
have hsuff : hd.weight ≥ d.weight := by
obtain ⟨m, hm, hmid, hmw⟩ := hmem
rcases List.mem_cons.mp hm with rfl | hmtl
· exact hmw
· exact absurd (hmid.trans h1.symm) (fun hc => (hd_notin m hmtl) (by rw [h1, ← hmid]))
have htl0 : (tl.map (lossOf d)).sum = 0 := by
apply List.sum_eq_zero
intro x hx; simp only [List.mem_map] at hx
obtain ⟨a, ha, rfl⟩ := hx
simp only [lossOf]; rw [if_neg]
intro hc; exact (hd_notin a ha) (by rw [hc, h1])
rw [htl0]; simp [Nat.min_eq_left hsuff]
· rw [if_neg h1]; simp only [Nat.zero_add]
apply ih h_tl_nodup
obtain ⟨m, hm, hmid, hmw⟩ := hmem
rcases List.mem_cons.mp hm with rfl | hmtl
· exact absurd hmid h1
· exact ⟨m, hmtl, hmid, hmw⟩
/-- With distinct ids, the delegatee present, and delegator ≠ delegatee,
the total weight added equals exactly `d.weight`. -/
theorem gain_sum (d : Delegation) (l : List Member)
(h_ne : d.delegator ≠ d.delegatee)
(h_nodup : l.Pairwise (fun a b => a.id ≠ b.id))
(hmem : ∃ m ∈ l, m.id = d.delegatee) :
(l.map (gainOf d)).sum = d.weight := by
induction l with
| nil => simp at hmem
| cons hd tl ih =>
rw [List.pairwise_cons] at h_nodup
obtain ⟨hd_notin, h_tl_nodup⟩ := h_nodup
simp only [List.map_cons, List.sum_cons, gainOf]
by_cases h2 : hd.id = d.delegatee
· have h1 : hd.id ≠ d.delegator := by rw [h2]; exact (Ne.symm h_ne)
rw [if_neg h1, if_pos h2]
have htl0 : (tl.map (gainOf d)).sum = 0 := by
apply List.sum_eq_zero
intro x hx; simp only [List.mem_map] at hx
obtain ⟨a, ha, rfl⟩ := hx
simp only [gainOf]
have : a.id ≠ d.delegatee := fun hc => (hd_notin a ha) (by rw [hc, h2])
rw [if_neg this]; split <;> rfl
rw [htl0, Nat.add_zero]
· by_cases h1 : hd.id = d.delegator
· rw [if_pos h1, Nat.zero_add]
apply ih h_tl_nodup
obtain ⟨m, hm, hmid⟩ := hmem
rcases List.mem_cons.mp hm with rfl | hmtl
· exact absurd hmid h2
· exact ⟨m, hmtl, hmid⟩
· rw [if_neg h1, if_neg h2, Nat.zero_add]
apply ih h_tl_nodup
obtain ⟨m, hm, hmid⟩ := hmem
rcases List.mem_cons.mp hm with rfl | hmtl
· exact absurd hmid h2
· exact ⟨m, hmtl, hmid⟩
/-- DELEGATION PRESERVES TOTAL WEIGHT: the sum of all member weights is
unchanged after delegation — weight is moved, not created or destroyed.
Preconditions (the actual requirements for conservation):
- the delegator and delegatee are distinct members,
- member ids are pairwise distinct,
- the delegator is present and holds at least `d.weight`,
- the delegatee is present (so the moved weight has a destination).
Proven honestly by a per-member ledger identity summed over the list. -/
theorem delegation_weight_preserved (members : List Member) (d : Delegation)
(h_ne : d.delegator ≠ d.delegatee)
(h_nodup : members.Pairwise (fun a b => a.id ≠ b.id))
(h_delegator : ∃ m ∈ members, m.id = d.delegator ∧ m.weight ≥ d.weight)
(h_delegatee : ∃ m ∈ members, m.id = d.delegatee) :
totalWeight (applyDelegation members d) = totalWeight members := by
rw [totalWeight_eq_sum, totalWeight_eq_sum]
have happ : applyDelegation members d = members.map (tf d) := rfl
rw [happ, List.map_map]
have hsb := sum_balance d members
have hl := loss_sum d members h_nodup h_delegator
have hg := gain_sum d members h_ne h_nodup h_delegatee
have hcomp : (members.map (Member.weight ∘ tf d))
= members.map (fun m => (tf d m).weight) := rfl
rw [hcomp]
rw [hl, hg] at hsb
omega
end Governance.DSO