proposal.gno
10.26 Kb · 282 lines
1package commondao
2
3import (
4 "errors"
5 "time"
6
7 "gno.land/p/nt/addrset/v0"
8)
9
10const (
11 StatusActive ProposalStatus = "active"
12 StatusPassed ProposalStatus = "passed"
13 StatusDismissed ProposalStatus = "dismissed"
14 StatusExecuted ProposalStatus = "executed"
15 StatusFailed ProposalStatus = "failed"
16 StatusWithdrawn ProposalStatus = "withdrawn"
17)
18
19// Vote choices, fixed by the Common DAO Spec's default voting rules.
20const (
21 ChoiceYes VoteChoice = "YES"
22 ChoiceNo VoteChoice = "NO"
23 ChoiceAbstain VoteChoice = "ABSTAIN"
24)
25
26// Thresholds for the constitution's default Council voting rules.
27const (
28 // ThresholdSupermajority passes with "two thirds or more" of the
29 // tally denominator. The default for Council decisions.
30 ThresholdSupermajority Threshold = iota
31
32 // ThresholdSimpleMajority passes with "more than half" of the
33 // tally denominator. The Constitution assigns it to specific
34 // decisions, e.g. sub-DAO creation.
35 ThresholdSimpleMajority
36)
37
38// Outcomes of tallying a proposal under the default Council rules.
39const (
40 OutcomePending Outcome = iota
41 OutcomePassed
42 OutcomeDismissed
43)
44
45var (
46 ErrInvalidCreatorAddress = errors.New("invalid proposal creator address")
47 ErrInvalidVoterAddress = errors.New("invalid voter address")
48 ErrProposalDefinitionRequired = errors.New("proposal definition is required")
49 ErrStatusIsNotActive = errors.New("proposal status is not active")
50)
51
52type (
53 // ProposalStatus defines a type for different proposal states.
54 ProposalStatus string
55
56 // VoteChoice defines a type for proposal vote choices.
57 VoteChoice string
58
59 // Threshold defines a type for the default tally thresholds.
60 Threshold int
61
62 // Outcome defines a type for default tally outcomes.
63 Outcome int
64
65 // ExecFunc defines a type for functions that execute proposals.
66 //
67 // The leading int makes ExecFunc non-crossing: the host calls it
68 // directly (no cross), so the executor holds no realm cur of its own —
69 // only the realm argument, a DAO-scoped sub-identity the host mints and
70 // passes. Fund-moving executors send through that sub (e.g. banker
71 // RealmSend), which is terminal and bounded to one DAO address;
72 // executors that move no funds ignore it. The int is unused.
73 //
74 // Authority note: the sub is a least-authority DEFAULT, not a sandbox.
75 // An executor is trusted realm code; because the sub is the executor's
76 // only current realm value, it could regain the host realm's primary
77 // authority via an explicit cross(sub) into a crossing function. That is
78 // a visible, auditable call the reference realm's executors never make,
79 // so their blast radius is one treasury — but a realm that runs
80 // untrusted or user-registered executors gets no such guarantee. See ADR
81 // pr6012_commondao_exec_scope.
82 //
83 // The sharper hazard for such a realm is not cross(sub) but the banker:
84 // an executor can mint banker.NewBanker(BankerTypeRealmSend, sub) and
85 // simply RETAIN it. Authorization happens at construction only, and the
86 // banker holds no realm reference, so it persists across transactions
87 // even though the sub itself cannot — a permanent, unrevocable
88 // capability over that DAO's address, spendable later with no proposal.
89 // It also bypasses any check the host performs before spending (a
90 // frozen flag, a pause switch), because it reaches the bank keeper
91 // without re-entering host code. Passing the sub to an executor whose
92 // code the DAO has not vetted is therefore an irrevocable grant of that
93 // DAO's treasury, not a scoped loan of it.
94 ExecFunc func(int, realm) error
95
96 // Proposal defines a DAO proposal.
97 Proposal struct {
98 id uint64
99 status ProposalStatus
100 definition ProposalDefinition
101 creator address
102 record *VotingRecord
103 electorate *addrset.Set // council snapshot taken at Propose
104 statusReason string
105 votingDeadline time.Time
106 createdAt time.Time
107 }
108
109 // ProposalDefinition defines an interface for custom proposal definitions.
110 // These definitions define proposal content and behavior, essentially
111 // allowing the definition of different proposal types.
112 ProposalDefinition interface {
113 // Title returns the proposal title.
114 Title() string
115
116 // Body returns proposal's body.
117 // It usually contains description or values that are specific to the proposal,
118 // like a description of the proposal's motivation or the list of values that
119 // would be applied when the proposal is approved.
120 Body() string
121
122 // VotingPeriod returns the period where votes are allowed after proposal creation.
123 // It is used to calculate the voting deadline from the proposal's creation date.
124 VotingPeriod() time.Duration
125
126 // Threshold returns the tally threshold for passing the proposal.
127 // Proposals are decided by the constitution's default Council voting
128 // rules: re-evaluated after every recorded vote, they can pass or be
129 // dismissed before their voting deadline.
130 //
131 // Threshold is read on every Vote (for early passage) AND again in
132 // the post-deadline re-tally inside Execute. Return a CONSTANT value:
133 // a threshold that loosens over a proposal's lifetime can let the
134 // deadline re-tally pass with fewer YES votes than voters faced when
135 // they cast under the stricter earlier value. A changing threshold is
136 // honored, but the definition author owns that consequence.
137 Threshold() Threshold
138 }
139
140 // ProposalKind defines an interface for proposal kinds: named factories
141 // for proposal definitions, registered per DAO. A kind is both the
142 // registry key (Name) and the factory (New) for one proposal type, and
143 // a DAO accepts proposals of exactly the kinds registered on it
144 // (CommonDAO.RegisterKind).
145 ProposalKind interface {
146 // Name returns the kind name used as registry key, e.g. "treasury-spend".
147 Name() string
148
149 // New validates args and builds the proposal definition. Propose
150 // passes a ReadonlyCommonDAO view of the host DAO, so New is a
151 // pure factory that cannot mutate the host or its tree before the
152 // vote; proposal targets and parameters come via args. A kind that
153 // must mutate state on execution receives the target *CommonDAO
154 // through args (which only trusted callers can populate), captures
155 // it, and mutates in its Executor. The returned definition's
156 // instance data is frozen at Propose like any proposal definition.
157 New(dao ReadonlyCommonDAO, args any) (ProposalDefinition, error)
158 }
159
160 // CapExempt defines an interface for proposal definitions that are not
161 // counted against the DAO's active proposals cap. Exempt definitions are
162 // instead bounded to one active proposal per creator, so that proposals
163 // which remove members (and therefore must never be blockable by a full
164 // cap) stay bounded.
165 CapExempt interface {
166 // CapExempt marks the definition as exempt.
167 CapExempt()
168 }
169
170 // Validable defines an interface for proposal definitions that require state validation.
171 // Validation is done before execution and normally also during proposal rendering.
172 Validable interface {
173 // Validate validates that the proposal is valid for the current state.
174 Validate() error
175 }
176
177 // Executable defines an interface for proposal definitions that modify state on approval.
178 // Once proposals are executed they are archived and considered finished.
179 Executable interface {
180 // Executor returns a function to execute the proposal.
181 Executor() ExecFunc
182 }
183)
184
185// newProposal creates a new DAO proposal.
186//
187// The proposal is created with an empty electorate; Propose populates it
188// with the council snapshot.
189func newProposal(id uint64, creator address, d ProposalDefinition) (*Proposal, error) {
190 if !creator.IsValid() {
191 return nil, ErrInvalidCreatorAddress
192 }
193
194 now := time.Now()
195 return &Proposal{
196 id: id,
197 status: StatusActive,
198 definition: d,
199 creator: creator,
200 record: &VotingRecord{},
201 electorate: &addrset.Set{},
202 votingDeadline: now.Add(d.VotingPeriod()),
203 createdAt: now,
204 }, nil
205}
206
207// ID returns the unique proposal identifier.
208func (p Proposal) ID() uint64 {
209 return p.id
210}
211
212// Definition returns the proposal definition.
213// Proposal definitions define proposal content and behavior.
214func (p Proposal) Definition() ProposalDefinition {
215 return p.definition
216}
217
218// Status returns the current proposal status.
219func (p Proposal) Status() ProposalStatus {
220 return p.status
221}
222
223// Creator returns the address of the account that created the proposal.
224func (p Proposal) Creator() address {
225 return p.creator
226}
227
228// CreatedAt returns the time that proposal was created.
229func (p Proposal) CreatedAt() time.Time {
230 return p.createdAt
231}
232
233// VotingRecord returns a read only record with the votes submitted for
234// the proposal. Votes are recorded through CommonDAO.Vote only.
235func (p Proposal) VotingRecord() ReadonlyVotingRecord {
236 return p.record.Readonly()
237}
238
239// Electorate returns the proposal's electorate: a read only view of the
240// council snapshot taken when the proposal was created. Members added to
241// the council afterwards vote on the next proposal; members removed or
242// resigned afterwards remain in the electorate (their silence counts
243// against passage).
244func (p Proposal) Electorate() *addrset.ReadonlySet {
245 return p.electorate.Readonly()
246}
247
248// StatusReason returns an optional reason that led to the current proposal status.
249// Reason is mostly useful when a proposal fails.
250func (p Proposal) StatusReason() string {
251 return p.statusReason
252}
253
254// VotingDeadline returns the deadline after which no more votes should be allowed.
255func (p Proposal) VotingDeadline() time.Time {
256 return p.votingDeadline
257}
258
259// HasVotingDeadlinePassed checks if the voting deadline has been met.
260func (p Proposal) HasVotingDeadlinePassed() bool {
261 return !time.Now().Before(p.VotingDeadline())
262}
263
264// Validate validates that a proposal is valid for the current state.
265// Validation is done when the proposal can still be executed (status is
266// active or passed) and when the definition supports validation.
267func (p Proposal) Validate() error {
268 if p.status != StatusActive && p.status != StatusPassed {
269 return nil
270 }
271
272 if v, ok := p.definition.(Validable); ok {
273 return v.Validate()
274 }
275 return nil
276}
277
278// ExpectedOutcome returns the outcome the proposal would have if it were
279// decided with the votes submitted so far. Useful for rendering.
280func (p Proposal) ExpectedOutcome() Outcome {
281 return TallyDefault(p.record.Readonly(), p.Electorate(), p.definition.Threshold())
282}