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