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}