proposal_bylaws.gno
7.21 Kb · 191 lines
1package commondao
2
3import (
4 "errors"
5 "strings"
6 "time"
7
8 "gno.land/p/moul/md/v0"
9 "gno.land/p/nt/bptree/v0"
10 "gno.land/p/nt/bylaws/v0"
11 "gno.land/p/nt/commondao/v0"
12)
13
14// bylawsSets stores each DAO's governing documents (bylaws and
15// mandates), keyed like daos. A set is created lazily on a DAO's first
16// amendment proposal; a DAO with no documents has no entry. The
17// *bylaws.Bylaws handle is mutable (Apply amends it), so it never leaves
18// the realm — public reads return strings.
19var bylawsSets = bptree.NewBPTree32() // string(ID) -> *bylaws.Bylaws
20
21// bylawsView returns a DAO's document set or nil when it has none. Read
22// paths (render, public getters, payload building) use this — it never
23// writes realm state, so it is safe under read-only query evaluation.
24func bylawsView(daoID uint64) *bylaws.Bylaws {
25 if v := bylawsSets.Get(makeIDKey(daoID)); v != nil {
26 return v.(*bylaws.Bylaws)
27 }
28 return nil
29}
30
31// bylawsOf returns a DAO's document set, creating an empty one on first
32// use. Only proposal creation calls it; an empty set left behind by a
33// failed proposal is harmless.
34func bylawsOf(daoID uint64) *bylaws.Bylaws {
35 if b := bylawsView(daoID); b != nil {
36 return b
37 }
38 b := bylaws.New()
39 bylawsSets.Set(makeIDKey(daoID), b)
40 return b
41}
42
43// isMandatesPath reports whether a document path is in the reserved
44// mandates/ folder (or is the bare "mandates" file). Mandates are not
45// council-self-amendable; see amendBylawsKind.New.
46func isMandatesPath(path string) bool {
47 return path == "mandates" || strings.HasPrefix(path, "mandates/")
48}
49
50// amendBylawsProposal is both the amend-bylaws args struct and the
51// proposal definition it produces (one type serves both roles, like
52// manageKindsProposal). set is the host DAO's document set the executor
53// patches: New receives only a readonly view, so the trusted wrapper
54// passes the mutable set through args (from its own bylawsOf) together
55// with the host daoID for the identity pin. display is the human-readable
56// change, rendered at New against the then-current document.
57type amendBylawsProposal struct {
58 daoID uint64
59 set *bylaws.Bylaws
60 patch bylaws.Patch
61 display string
62}
63
64// amendBylawsKind creates proposals that add, amend or remove one of the
65// host DAO's bylaws/mandates documents with a verifiable diff patch. The
66// patch pins the sha256 of the document text it was diffed against, so a
67// passed amendment that raced a concurrent change to the same document
68// fails cleanly (StatusFailed) instead of clobbering it. Decided by
69// supermajority: this is the council amending its OWN governing
70// documents, which the Constitution grants with no special threshold, so
71// the default council rule applies. (The Constitution's simple-majority
72// clause for charter/bylaws/mandate changes is an ANCESTOR power — a
73// parent amending a descendant's documents — which this realm does not
74// implement yet.)
75type amendBylawsKind struct{}
76
77func (amendBylawsKind) Name() string { return kindAmendBylaws }
78
79func (amendBylawsKind) New(dao commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {
80 p, ok := args.(amendBylawsProposal)
81 if !ok {
82 return nil, errInvalidProposalArgs
83 }
84 // Defense in depth: pin the proposal to the readonly host Propose
85 // passed here AND pin the args-captured document set to that host's
86 // canonical set, so a future wrapper can never validate against one
87 // DAO's documents and amend another's (the trusted wrapper always
88 // passes matching handles today).
89 if p.daoID != dao.ID() || p.set == nil || p.set != bylawsView(p.daoID) {
90 return nil, errInvalidProposalArgs
91 }
92 if !bylaws.IsValidPath(p.patch.Path) {
93 return nil, bylaws.ErrInvalidPath
94 }
95 // The Constitution grants a council self-power over its BYLAWS only;
96 // Mandates are changed from above (creation or an ancestor's Simple
97 // Majority — the ancestor amendment path, not implemented yet). Reserve
98 // the mandates/ folder so self-amendment cannot author what only an
99 // ancestor may.
100 if isMandatesPath(p.patch.Path) {
101 return nil, errors.New("mandates are not council-amendable: they are set at creation or by an ancestor (ancestor amendment is not implemented yet)")
102 }
103
104 // Freshness fail-fast: reject a patch that is already stale at
105 // creation (Validate re-checks at execution).
106 cur, exists := p.set.Get(p.patch.Path)
107 curHash := ""
108 if exists {
109 curHash = bylaws.HashText(cur)
110 }
111 if p.patch.Base != curHash {
112 return nil, bylaws.ErrStalePatch
113 }
114
115 // Rendering the change also validates the edit script against the
116 // current text, so a malformed patch never becomes a proposal; only
117 // then reject the well-formed do-nothing shapes.
118 display, err := p.patch.Format(cur)
119 if err != nil {
120 return nil, err
121 }
122 if p.patch.IsNoop() {
123 return nil, errors.New("bylaws amendment must change the document")
124 }
125 p.display = display
126 return p, nil
127}
128
129// Title returns the proposal title as raw text: the renderer escapes
130// every definition title.
131func (p amendBylawsProposal) Title() string {
132 verb := "Amend"
133 switch {
134 case p.patch.IsCreate():
135 verb = "Add"
136 case p.patch.IsRemove():
137 verb = "Remove"
138 }
139 return verb + " Bylaws Document: " + p.patch.Path
140}
141
142// isTrustedMarkdownBody marks Body as self-assembled markdown: the path
143// is escaped inline and the change summary is emitted as a fenced code
144// block.
145func (amendBylawsProposal) isTrustedMarkdownBody() {}
146
147func (p amendBylawsProposal) Body() string {
148 // A code block, not sanitize.Block: the summary's "+" lines are the
149 // proposer's inserted literal text, and Block deliberately preserves
150 // inline formatting AND inline links — which would render a live
151 // attacker-controlled link on the page councils read before voting
152 // (the same reason renderProposal escapes untrusted bodies inline).
153 // A fence keeps the diff's line structure, which an inline escape
154 // would fold away, and neutralizes markup; md.CodeBlock widens the
155 // fence to outscan any backticks in the content.
156 return md.Paragraph(md.Bold("Document:")+" "+md.EscapeText(p.patch.Path)) +
157 md.CodeBlock(p.display)
158}
159
160func (amendBylawsProposal) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }
161
162// Threshold returns the tally threshold: amending the DAO's own
163// governing documents is a council decision with no special
164// constitutional threshold, so the supermajority default applies (the
165// Constitution's simple-majority clause covers ancestor-initiated
166// amendment, not implemented here).
167func (amendBylawsProposal) Threshold() commondao.Threshold {
168 return commondao.ThresholdSupermajority
169}
170
171// Validate re-asserts patch freshness at execution (Validate reruns
172// inside Execute): a document amended after this proposal passed fails
173// it cleanly (StatusFailed) instead of clobbering the newer text.
174func (p amendBylawsProposal) Validate() error {
175 if p.set.Hash(p.patch.Path) != p.patch.Base {
176 return bylaws.ErrStalePatch
177 }
178 return nil
179}
180
181func (p amendBylawsProposal) Executor() commondao.ExecFunc {
182 return p.execute
183}
184
185// execute applies the patch, returning any bylaws error unchanged so a
186// race between two passed amendments fails the later one cleanly
187// (StatusFailed) instead of panicking the transaction. It moves no
188// funds, so the definition is not Funded and ignores sub.
189func (p amendBylawsProposal) execute(_ int, _ realm) error {
190 return p.set.Apply(p.patch)
191}