mirror of
https://github.com/zooai/proofs.git
synced 2026-10-30 14:49:47 +00:00
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]>
345 lines
14 KiB
Lean4
345 lines
14 KiB
Lean4
/-
|
||
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
|