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_treasury.gno

9.93 Kb · 298 lines
  1package commondao
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"errors"
  7	"strings"
  8	"time"
  9
 10	"gno.land/p/moul/md/v0"
 11	"gno.land/p/nt/commondao/v0"
 12)
 13
 14// hasLiveProperAncestor checks if any proper ancestor of dao is not
 15// dissolved.
 16func hasLiveProperAncestor(dao *commondao.CommonDAO) bool {
 17	for p := dao.Parent(); p != nil; p = p.Parent() {
 18		if !p.IsDeleted() {
 19			return true
 20		}
 21	}
 22	return false
 23}
 24
 25// isProperAncestor checks if dao is a proper ancestor of target.
 26// Parent pointers are set only at construction and no re-parenting path
 27// exists, so ancestry is stable for the lifetime of a proposal.
 28func isProperAncestor(dao, target *commondao.CommonDAO) bool {
 29	for p := target.Parent(); p != nil; p = p.Parent() {
 30		if p.ID() == dao.ID() {
 31			return true
 32		}
 33	}
 34	return false
 35}
 36
 37// assertIsProperAncestor validates the ancestor relation that authorizes
 38// treasury controls over a descendant (docs/CONSTITUTION.md :1507). The
 39// relation is strictly proper: a DAO can never claw back or unfreeze
 40// itself.
 41func assertIsProperAncestor(dao, target *commondao.CommonDAO) error {
 42	if target.ID() == dao.ID() {
 43		return errors.New("a DAO cannot target itself")
 44	}
 45	if !isProperAncestor(dao, target) {
 46		return errors.New("DAO is not an ancestor of the target DAO")
 47	}
 48	return nil
 49}
 50
 51// treasuryBalance returns the current balance of a DAO's treasury address.
 52func treasuryBalance(dao *commondao.CommonDAO) chain.Coins {
 53	return banker.NewReadonlyBanker().GetCoins(dao.Address())
 54}
 55
 56// newTreasurySpendPropDefinition creates a proposal definition that sends
 57// coins from the DAO's own treasury.
 58func newTreasurySpendPropDefinition(dao *commondao.CommonDAO, to address, coin chain.Coin) treasurySpendPropDefinition {
 59	if dao == nil {
 60		panic("DAO is required")
 61	}
 62	if !to.IsValid() {
 63		panic("invalid recipient address")
 64	}
 65	if coin.Denom == "" {
 66		panic("coin denomination is empty")
 67	}
 68	if !coin.IsPositive() {
 69		panic("spend amount must be positive")
 70	}
 71
 72	return treasurySpendPropDefinition{dao, to, coin}
 73}
 74
 75// treasurySpendPropDefinition defines a proposal type for spending funds
 76// from the DAO's own treasury (docs/CONSTITUTION.md :1542-1543).
 77type treasurySpendPropDefinition struct {
 78	dao  *commondao.CommonDAO
 79	to   address
 80	coin chain.Coin
 81}
 82
 83func (treasurySpendPropDefinition) Title() string               { return "Treasury Spend" }
 84func (treasurySpendPropDefinition) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }
 85
 86// isTrustedMarkdownBody marks Body as self-assembled markdown; the embedded
 87// recipient and amount are formatted by md helpers / EscapeText in Body.
 88func (treasurySpendPropDefinition) isTrustedMarkdownBody() {}
 89
 90// Threshold returns the tally threshold: the constitution attaches no
 91// spend-specific rule, so the supermajority default applies.
 92func (treasurySpendPropDefinition) Threshold() commondao.Threshold {
 93	return commondao.ThresholdSupermajority
 94}
 95
 96func (p treasurySpendPropDefinition) Body() string {
 97	var b strings.Builder
 98
 99	b.WriteString(md.Paragraph(md.Bold("Recipient:") + "\n" + userLink(p.to)))
100	b.WriteString(md.Paragraph(md.Bold("Amount:") + "\n" + md.EscapeText(p.coin.String())))
101
102	return b.String()
103}
104
105// Validate runs at proposal creation and again at execution, so a
106// treasury frozen or drained after the proposal passed still fails it
107// cleanly (StatusFailed, coins untouched) instead of panicking the tx.
108func (p treasurySpendPropDefinition) Validate() error {
109	if p.dao.IsDeleted() {
110		return errors.New("DAO has already been dissolved")
111	}
112	if p.dao.IsTreasuryFrozen() {
113		return errors.New("DAO treasury is frozen")
114	}
115	if treasuryBalance(p.dao).AmountOf(p.coin.Denom) < p.coin.Amount {
116		return errors.New("insufficient treasury balance")
117	}
118	return nil
119}
120
121func (p treasurySpendPropDefinition) Executor() commondao.ExecFunc {
122	return p.execute
123}
124
125// FundingDAOID returns the ID of the DAO whose treasury funds the spend:
126// its own (the host).
127func (p treasurySpendPropDefinition) FundingDAOID() uint64 {
128	return p.dao.ID()
129}
130
131func (p treasurySpendPropDefinition) execute(_ int, sub realm) error {
132	// sub is this DAO's sub-identity, minted by the host: send from it.
133	// Banker sends move bank balances without invoking recipient code, so
134	// there is no reentrancy vector. Validate ran in this same Execute
135	// call, so the balance check is current.
136	b := banker.NewBanker(banker.BankerTypeRealmSend, sub)
137	b.SendCoins(sub.Address(), p.to, chain.NewCoins(p.coin))
138	return nil
139}
140
141// newTreasuryClawbackPropDefinition creates a proposal definition that
142// sweeps a descendant DAO's treasury one step up the tree.
143func newTreasuryClawbackPropDefinition(dao, target *commondao.CommonDAO) treasuryClawbackPropDefinition {
144	if dao == nil {
145		panic("DAO is required")
146	}
147	if target == nil {
148		panic("target DAO is required")
149	}
150
151	return treasuryClawbackPropDefinition{dao, target}
152}
153
154// treasuryClawbackPropDefinition defines a proposal type for an ancestor
155// DAO to reclaim a descendant's treasury. The destination is fixed — the
156// target's parent — so funds move one step up the tree toward their
157// origin and can never be drained out of the tree entirely. Clawback
158// remains valid against soft-deleted and frozen descendants.
159type treasuryClawbackPropDefinition struct {
160	dao    *commondao.CommonDAO // proposing DAO, must be a proper ancestor
161	target *commondao.CommonDAO
162}
163
164func (treasuryClawbackPropDefinition) Title() string               { return "Treasury Clawback" }
165func (treasuryClawbackPropDefinition) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }
166
167// isTrustedMarkdownBody marks Body as self-assembled markdown (a DAO link).
168func (treasuryClawbackPropDefinition) isTrustedMarkdownBody() {}
169
170// Threshold returns the tally threshold: simple majority, the
171// constitutional wording for this ancestor power.
172func (treasuryClawbackPropDefinition) Threshold() commondao.Threshold {
173	return commondao.ThresholdSimpleMajority
174}
175
176func (p treasuryClawbackPropDefinition) Body() string {
177	var b strings.Builder
178
179	b.WriteString(md.Paragraph(md.Bold("Target DAO:") + "\n" + daoMDLink(p.target)))
180	b.WriteString(md.Paragraph(
181		md.Bold("Destination:") + "\n" +
182			"The target's parent DAO receives the target's full balance at execution time.",
183	))
184
185	return b.String()
186}
187
188func (p treasuryClawbackPropDefinition) Validate() error {
189	return assertIsProperAncestor(p.dao, p.target)
190}
191
192func (p treasuryClawbackPropDefinition) Executor() commondao.ExecFunc {
193	return p.execute
194}
195
196// FundingDAOID returns the ID of the DAO whose treasury the clawback
197// sweeps: the target, not the proposing ancestor that hosts the proposal.
198func (p treasuryClawbackPropDefinition) FundingDAOID() uint64 {
199	return p.target.ID()
200}
201
202func (p treasuryClawbackPropDefinition) execute(_ int, sub realm) error {
203	balance := treasuryBalance(p.target)
204	if balance.IsZero() {
205		return nil
206	}
207
208	// sub is the target's sub-identity, minted by the host. A proper
209	// ancestor exists, so the target always has a parent. Full-balance
210	// send: cost is O(number of denoms held). A target dusted with many
211	// realm-minted denoms can push this past block gas (known limitation —
212	// see the treasury ADR §Sweep gas-bomb).
213	b := banker.NewBanker(banker.BankerTypeRealmSend, sub)
214	b.SendCoins(sub.Address(), p.target.Parent().Address(), balance)
215	return nil
216}
217
218// newTreasuryFreezePropDefinition creates a proposal definition that
219// freezes or unfreezes a descendant DAO's treasury.
220func newTreasuryFreezePropDefinition(dao, target *commondao.CommonDAO, frozen bool) treasuryFreezePropDefinition {
221	if dao == nil {
222		panic("DAO is required")
223	}
224	if target == nil {
225		panic("target DAO is required")
226	}
227
228	return treasuryFreezePropDefinition{dao, target, frozen}
229}
230
231// treasuryFreezePropDefinition defines a proposal type for an ancestor
232// DAO to freeze or unfreeze a descendant's treasury. Freezing does not
233// cascade: ancestors freeze each descendant explicitly. Only a proper
234// ancestor can unfreeze — the frozen DAO's own council cannot.
235type treasuryFreezePropDefinition struct {
236	dao    *commondao.CommonDAO // proposing DAO, must be a proper ancestor
237	target *commondao.CommonDAO
238	frozen bool
239}
240
241func (p treasuryFreezePropDefinition) Title() string {
242	if p.frozen {
243		return "Treasury Freeze"
244	}
245	return "Treasury Unfreeze"
246}
247
248func (treasuryFreezePropDefinition) VotingPeriod() time.Duration { return time.Hour * 24 * 7 }
249
250// isTrustedMarkdownBody marks Body as self-assembled markdown (a DAO link).
251func (treasuryFreezePropDefinition) isTrustedMarkdownBody() {}
252
253// Threshold returns the tally threshold: simple majority, matching the
254// clawback power it safeguards.
255func (treasuryFreezePropDefinition) Threshold() commondao.Threshold {
256	return commondao.ThresholdSimpleMajority
257}
258
259func (p treasuryFreezePropDefinition) Body() string {
260	var b strings.Builder
261
262	b.WriteString(md.Paragraph(md.Bold("Target DAO:") + "\n" + daoMDLink(p.target)))
263
264	action := "frozen: no funds can leave the treasury until a proper ancestor unfreezes it."
265	if !p.frozen {
266		action = "unfrozen: funds can leave the treasury again."
267	}
268	b.WriteString(md.Paragraph(md.Bold("Effect:") + "\nThe target's treasury is " + action))
269
270	return b.String()
271}
272
273func (p treasuryFreezePropDefinition) Validate() error {
274	// Orphan rescue: when every proper ancestor is dissolved, the freezing
275	// authority class is extinct, so the target's own council may restore
276	// the constitutional default by unfreezing itself. Without this, a
277	// frozen DAO orphaned by its ancestors' dissolution would hold its
278	// funds locked forever. Freezing - and any self-targeting while a
279	// live ancestor exists - stays ancestor-only.
280	if !p.frozen && p.dao.ID() == p.target.ID() && !hasLiveProperAncestor(p.target) {
281		return nil
282	}
283	return assertIsProperAncestor(p.dao, p.target)
284}
285
286func (p treasuryFreezePropDefinition) Executor() commondao.ExecFunc {
287	return p.execute
288}
289
290func (p treasuryFreezePropDefinition) execute(_ int, sub realm) error {
291	p.target.SetTreasuryFrozen(p.frozen)
292	return nil
293}
294
295// daoMDLink returns a markdown link to a DAO's page.
296func daoMDLink(dao *commondao.CommonDAO) string {
297	return md.Link(dao.Name(), daoURL(dao.ID()))
298}