proposal.gno
7.85 Kb ยท 268 lines
1package commondao
2
3import (
4 "errors"
5 "std"
6 "time"
7
8 "gno.land/p/nt/avl"
9)
10
11const (
12 StatusActive ProposalStatus = "active"
13 StatusFailed = "failed"
14 StatusPassed = "passed"
15)
16
17const (
18 ChoiceNone VoteChoice = ""
19 ChoiceYes = "YES"
20 ChoiceNo = "NO"
21 ChoiceNoWithVeto = "NO WITH VETO"
22 ChoiceAbstain = "ABSTAIN"
23)
24
25const (
26 QuorumOneThird float64 = 0.33 // percentage
27 QuorumHalf = 0.5
28 QuorumTwoThirds = 0.66
29 QuorumThreeFourths = 0.75
30 QuorumFull = 1
31)
32
33// MaxCustomVoteChoices defines the maximum number of custom
34// vote choices that a proposal definition can define.
35const MaxCustomVoteChoices = 10
36
37var (
38 ErrInvalidCreatorAddress = errors.New("invalid proposal creator address")
39 ErrMaxCustomVoteChoices = errors.New("max number of custom vote choices exceeded")
40 ErrProposalDefinitionRequired = errors.New("proposal definition is required")
41 ErrNoQuorum = errors.New("no quorum")
42 ErrStatusIsNotActive = errors.New("proposal status is not active")
43)
44
45type (
46 // ProposalStatus defines a type for different proposal states.
47 ProposalStatus string
48
49 // VoteChoice defines a type for proposal vote choices.
50 VoteChoice string
51
52 // Proposal defines a DAO proposal.
53 Proposal struct {
54 id uint64
55 status ProposalStatus
56 definition ProposalDefinition
57 creator std.Address
58 record *VotingRecord // TODO: Add support for multiple voting records
59 statusReason string
60 voteChoices *avl.Tree // string(VoteChoice) -> struct{}
61 votingDeadline time.Time
62 createdAt time.Time
63 }
64
65 // ProposalDefinition defines an interface for custom proposal definitions.
66 // These definitions define proposal content and behavior, they esentially
67 // allow the definition for different proposal types.
68 ProposalDefinition interface {
69 // Title returns the proposal title.
70 Title() string
71
72 // Body returns proposal's body.
73 // It usually contains description or values that are specific to the proposal,
74 // like a description of the proposal's motivation or the list of values that
75 // would be applied when the proposal is approved.
76 Body() string
77
78 // VotingPeriod returns the period where votes are allowed after proposal creation.
79 // It is used to calculate the voting deadline from the proposal's creationd date.
80 VotingPeriod() time.Duration
81
82 // Tally counts the number of votes and verifies if proposal passes.
83 // It receives a readonly record containing the votes that has been
84 // submitted for the proposal and also the list of current DAO members.
85 Tally(ReadonlyVotingRecord, MemberSet) (passes bool, _ error)
86 }
87
88 // Validable defines an interface for proposal definitions that require state validation.
89 // Validation is done before execution and normally also during proposal rendering.
90 Validable interface {
91 // Validate validates that the proposal is valid for the current state.
92 Validate() error
93 }
94
95 // Executable defines an interface for proposal definitions that modify state on approval.
96 // Once proposals are executed they are archived and considered finished.
97 Executable interface {
98 // Execute executes the proposal.
99 Execute(realm) error
100 }
101
102 // CustomizableVoteChoices defines an interface for proposal definitions that want
103 // to customize the list of allowed voting choices.
104 CustomizableVoteChoices interface {
105 // CustomVoteChoices returns a list of valid voting choices.
106 // Choices are considered valid only when there are at least two possible choices
107 // otherwise proposal defaults to using YES, NO and ABSTAIN as valid choices.
108 CustomVoteChoices() []VoteChoice
109 }
110)
111
112// MustValidate validates that a proposal is valid for the current state or panics on error.
113func MustValidate(v Validable) {
114 if v == nil {
115 panic("validable proposal definition is nil")
116 }
117
118 if err := v.Validate(); err != nil {
119 panic(err)
120 }
121}
122
123// MustExecute executes an executable proposal or panics on error.
124func MustExecute(e Executable) {
125 if e == nil {
126 panic("executable proposal definition is nil")
127 }
128
129 if err := e.Execute(cross); err != nil {
130 panic(err)
131 }
132}
133
134// NewProposal creates a new DAO proposal.
135func NewProposal(id uint64, creator std.Address, d ProposalDefinition) (*Proposal, error) {
136 if d == nil {
137 return nil, ErrProposalDefinitionRequired
138 }
139
140 if !creator.IsValid() {
141 return nil, ErrInvalidCreatorAddress
142 }
143
144 now := time.Now()
145 p := &Proposal{
146 id: id,
147 status: StatusActive,
148 definition: d,
149 creator: creator,
150 record: &VotingRecord{},
151 voteChoices: avl.NewTree(),
152 votingDeadline: now.Add(d.VotingPeriod()),
153 createdAt: now,
154 }
155
156 if v, ok := d.(CustomizableVoteChoices); ok {
157 choices := v.CustomVoteChoices()
158 if len(choices) > MaxCustomVoteChoices {
159 return nil, ErrMaxCustomVoteChoices
160 }
161
162 for _, c := range choices {
163 p.voteChoices.Set(string(c), struct{}{})
164 }
165 }
166
167 // Use default voting choices when the definition returns none or a single vote choice
168 if p.voteChoices.Size() < 2 {
169 p.voteChoices.Set(string(ChoiceYes), struct{}{})
170 p.voteChoices.Set(string(ChoiceNo), struct{}{})
171 p.voteChoices.Set(string(ChoiceAbstain), struct{}{})
172 }
173 return p, nil
174}
175
176// ID returns the unique proposal identifies.
177func (p Proposal) ID() uint64 {
178 return p.id
179}
180
181// Definition returns the proposal definition.
182// Proposal definitions define proposal content and behavior.
183func (p Proposal) Definition() ProposalDefinition {
184 return p.definition
185}
186
187// Status returns the current proposal status.
188func (p Proposal) Status() ProposalStatus {
189 return p.status
190}
191
192// Creator returns the address of the account that created the proposal.
193func (p Proposal) Creator() std.Address {
194 return p.creator
195}
196
197// CreatedAt returns the time that proposal was created.
198func (p Proposal) CreatedAt() time.Time {
199 return p.createdAt
200}
201
202// VotingRecord returns a record that contains all the votes submitted for the proposal.
203func (p Proposal) VotingRecord() *VotingRecord {
204 return p.record
205}
206
207// StatusReason returns an optional reason that lead to the current proposal status.
208// Reason is mostyl useful when a proposal fails.
209func (p Proposal) StatusReason() string {
210 return p.statusReason
211}
212
213// VotingDeadline returns the deadline after which no more votes should be allowed.
214func (p Proposal) VotingDeadline() time.Time {
215 return p.votingDeadline
216}
217
218// VoteChoices returns the list of vote choices allowed for the proposal.
219func (p Proposal) VoteChoices() []VoteChoice {
220 choices := make([]VoteChoice, 0, p.voteChoices.Size())
221 p.voteChoices.Iterate("", "", func(c string, _ any) bool {
222 choices = append(choices, VoteChoice(c))
223 return false
224 })
225 return choices
226}
227
228// HasVotingDeadlinePassed checks if the voting deadline has been met.
229func (p Proposal) HasVotingDeadlinePassed() bool {
230 return !time.Now().Before(p.VotingDeadline())
231}
232
233// Validate validates that a proposal is valid for the current state.
234// Validation is done when proposal status is active and when the definition supports validation.
235func (p Proposal) Validate() error {
236 if p.status != StatusActive {
237 return nil
238 }
239
240 if v, ok := p.definition.(Validable); ok {
241 return v.Validate()
242 }
243 return nil
244}
245
246// IsVoteChoiceValid checks if a vote choice is valid for the proposal.
247func (p Proposal) IsVoteChoiceValid(c VoteChoice) bool {
248 return p.voteChoices.Has(string(c))
249}
250
251// IsQuorumReached checks if a participation quorum is reach.
252func IsQuorumReached(quorum float64, r ReadonlyVotingRecord, members MemberSet) bool {
253 if members.Size() <= 0 || quorum <= 0 {
254 return false
255 }
256
257 var totalCount int
258 r.IterateVotesCount(func(c VoteChoice, voteCount int) bool {
259 // Don't count explicit abstentions or invalid votes
260 if c != ChoiceNone && c != ChoiceAbstain {
261 totalCount += r.VoteCount(c)
262 }
263 return false
264 })
265
266 percentage := float64(totalCount) / float64(members.Size())
267 return percentage >= quorum
268}