proposal_subdao.gno
7.31 Kb · 225 lines
1package commondao
2
3import (
4 "chain/banker"
5 "errors"
6 "strings"
7 "time"
8
9 "gno.land/p/moul/md/v0"
10 "gno.land/p/nt/commondao/v0"
11)
12
13// newSubDAOPropDefinition creates a new proposal definition for adding a SubDAO.
14func newSubDAOPropDefinition(parent *commondao.CommonDAO, name, purpose, description string, members []address) subDAOPropDefinition {
15 if parent == nil {
16 panic("parent DAO is required")
17 }
18
19 name = strings.TrimSpace(name)
20 assertDAONameIsValid(name)
21
22 purpose = strings.TrimSpace(purpose)
23 assertDAOPurposeIsValid(purpose)
24
25 description = strings.TrimSpace(description)
26 assertDAODescriptionIsValid(description)
27
28 if len(members) == 0 {
29 panic("a SubDAO requires at least one initial council member")
30 }
31
32 return subDAOPropDefinition{
33 parent: parent,
34 name: name,
35 purpose: purpose,
36 description: description,
37 members: members,
38 }
39}
40
41// subDAOPropDefinition defines a proposal type for adding a SubDAO.
42type subDAOPropDefinition struct {
43 parent *commondao.CommonDAO
44 name string
45 purpose string
46 description string
47 members []address
48}
49
50func (p subDAOPropDefinition) Title() string { return "New SubDAO: " + p.name }
51func (subDAOPropDefinition) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }
52
53// isTrustedMarkdownBody marks Body as self-assembled markdown; the embedded
54// user fields (name, purpose, description) are escaped in Body itself.
55func (subDAOPropDefinition) isTrustedMarkdownBody() {}
56
57// Threshold returns the tally threshold: the constitution grants SubDAO
58// creation at a simple majority of the Council.
59func (subDAOPropDefinition) Threshold() commondao.Threshold {
60 return commondao.ThresholdSimpleMajority
61}
62
63func (p subDAOPropDefinition) Body() string {
64 var b strings.Builder
65
66 b.WriteString(md.Paragraph(
67 md.Bold("Parent DAO:") + "\n" + daoMDLink(p.parent),
68 ))
69
70 b.WriteString(md.Paragraph(
71 md.Bold("SubDAO Name:") + "\n" + md.EscapeText(p.name),
72 ))
73
74 b.WriteString(md.Paragraph(
75 md.Bold("SubDAO Purpose:") + "\n" + md.EscapeText(p.purpose),
76 ))
77
78 if p.description != "" {
79 b.WriteString(md.Paragraph(
80 md.Bold("SubDAO Description:") + "\n" + md.EscapeText(p.description),
81 ))
82 }
83
84 b.WriteString(md.Paragraph(
85 md.Bold("Council Members:") + "\n" + md.BulletList(addrStrings(p.members)),
86 ))
87
88 return b.String()
89}
90
91func (p subDAOPropDefinition) Validate() (err error) {
92 p.parent.IterateChildren(func(subDAO *commondao.CommonDAO) bool {
93 if subDAO.Name() == p.name {
94 err = errors.New("a SubDAO with the same name already exists")
95 return true
96 }
97 return false
98 })
99 return err
100}
101
102func (p subDAOPropDefinition) Executor() commondao.ExecFunc {
103 return p.execute
104}
105
106func (p subDAOPropDefinition) execute(_ int, sub realm) error {
107 createSubDAO(p.parent, p.name, p.purpose, p.description, p.members...)
108 return nil
109}
110
111// newDissolvePropDefinition creates a new proposal definition for
112// dissolving a DAO. Dissolution sweeps any remaining treasury balance:
113// sub-DAO sweeps go to the parent (fixed, not nameable — the same place
114// a clawback would put the funds); a root DAO has no parent, so its
115// dissolution requires an explicit destination. Both rules are
116// state-independent: parent pointers never change after construction.
117func newDissolvePropDefinition(dao *commondao.CommonDAO, destination address) dissolvePropDefinition {
118 if dao == nil {
119 panic("SubDAO is required")
120 }
121 if dao.Parent() != nil {
122 if destination != "" {
123 panic("sub-DAO dissolution sweeps to the parent DAO; destination must be empty")
124 }
125 } else {
126 if !destination.IsValid() {
127 panic("root DAO dissolution requires a valid sweep destination")
128 }
129 }
130
131 return dissolvePropDefinition{dao, destination}
132}
133
134// dissolvePropDefinition defines a proposal type for dissolving a SubDAO.
135type dissolvePropDefinition struct {
136 dao *commondao.CommonDAO
137 destination address // sweep destination, root DAOs only
138}
139
140func (p dissolvePropDefinition) Title() string { return "Dissolve DAO: " + p.dao.Name() }
141func (dissolvePropDefinition) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }
142
143// isTrustedMarkdownBody marks Body as self-assembled markdown (a DAO link).
144func (dissolvePropDefinition) isTrustedMarkdownBody() {}
145
146// Threshold returns the tally threshold: the constitution is silent on
147// dissolution, so the supermajority default applies.
148func (dissolvePropDefinition) Threshold() commondao.Threshold {
149 return commondao.ThresholdSupermajority
150}
151
152func (p dissolvePropDefinition) Body() string {
153 var b strings.Builder
154
155 b.WriteString(md.Paragraph(md.Bold("DAO:") + "\n" + daoMDLink(p.dao)))
156
157 // A root dissolution sweeps the entire treasury to an address named at
158 // proposal creation. Show it — it is the one dissolution shape with a
159 // free-form destination, and voters cannot judge the proposal without
160 // it (a spend renders its recipient for the same reason). Sub-DAO
161 // dissolution has no destination: it always sweeps up the tree.
162 if p.destination != "" {
163 b.WriteString(md.Paragraph(md.Bold("Sweep destination:") + "\n" + userLink(p.destination)))
164 }
165
166 return b.String()
167}
168
169func (p dissolvePropDefinition) Validate() (err error) {
170 if p.dao.IsDeleted() {
171 return errors.New("DAO has already been dissolved")
172 }
173 return nil
174}
175
176func (p dissolvePropDefinition) Executor() commondao.ExecFunc {
177 return p.execute
178}
179
180// FundingDAOID returns the ID of the DAO whose treasury the dissolution
181// sweeps: the DAO being dissolved. For a sub-DAO the proposal is hosted in
182// an ancestor, so the operative DAO differs from the host.
183func (p dissolvePropDefinition) FundingDAOID() uint64 {
184 return p.dao.ID()
185}
186
187func (p dissolvePropDefinition) execute(_ int, sub realm) error {
188 // Sweep any remaining treasury before soft deleting: no council
189 // remains afterwards to pass a spend. Sub-DAO funds go to the parent;
190 // root funds go to the destination named at proposal creation. A
191 // frozen treasury does not block dissolution — the sweep sends the
192 // funds where a clawback would. sub is the dissolved DAO's sub-identity,
193 // minted by the host. Full-balance send: cost is O(denoms); a
194 // denom-flooded DAO can push this past block gas, blocking dissolution
195 // (known limitation — see the treasury ADR §Sweep gas-bomb).
196 if balance := treasuryBalance(p.dao); !balance.IsZero() {
197 dest := p.destination
198 if parent := p.dao.Parent(); parent != nil {
199 // Sweep to the nearest LIVE ancestor, mirroring the walk
200 // CreateDissolutionProposal uses to pick the host. Sending to a
201 // dissolved intermediate would deposit the balance on a dead
202 // DAO: only a further clawback could rescue it, and once the
203 // whole chain is dissolved it is lost outright. A dissolution
204 // proposal can only be hosted by a live ancestor (a deleted DAO
205 // rejects Propose), so one always exists.
206 for parent.IsDeleted() && parent.Parent() != nil {
207 parent = parent.Parent()
208 }
209 dest = parent.Address()
210 }
211
212 b := banker.NewBanker(banker.BankerTypeRealmSend, sub)
213 b.SendCoins(sub.Address(), dest, balance)
214 }
215
216 // Drop the DAO from the home index: a dissolved DAO can no longer be
217 // unlisted through SetListed (it rejects deleted DAOs), so unlist it
218 // here rather than leave it stuck in the listing.
219 setListed(p.dao.ID(), false)
220
221 // Dissolution voids the DAO's in-flight proposals before soft deleting
222 // it, so nothing remains pending on a DAO that rejects execution.
223 p.dao.Dissolve("DAO dissolved")
224 return nil
225}