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_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}