Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}