commondao.gno
7.25 Kb · 277 lines
1package commondao
2
3import (
4 "errors"
5
6 "gno.land/p/nt/avl/v0/list"
7 "gno.land/p/nt/seqid/v0"
8)
9
10// PathSeparator is the separator character used in DAO paths.
11const PathSeparator = "/"
12
13var (
14 ErrExecutionNotAllowed = errors.New("proposal must pass before execution")
15 ErrInvalidVoteChoice = errors.New("invalid vote choice")
16 ErrNotMember = errors.New("account is not a member of the DAO")
17 ErrOverflow = errors.New("next ID overflows uint64")
18 ErrProposalNotFound = errors.New("proposal not found")
19 ErrVotingDeadlineNotMet = errors.New("voting deadline not met")
20 ErrVotingDeadlinePassed = errors.New("voting deadline has passed")
21 ErrWithdrawalNotAllowed = errors.New("withdrawal not allowed for proposals with votes")
22)
23
24// CommonDAO defines a DAO.
25type CommonDAO struct {
26 id uint64
27 slug string
28 name string
29 description string
30 parent *CommonDAO
31 children list.IList
32 members MemberStorage
33 genID seqid.ID
34 activeProposals ProposalStorage
35 finishedProposals ProposalStorage
36 deleted bool // Soft delete
37 disableVotingDeadlineCheck bool
38}
39
40// New creates a new common DAO.
41func New(options ...Option) *CommonDAO {
42 dao := &CommonDAO{
43 children: &list.List{},
44 members: NewMemberStorage(),
45 activeProposals: NewProposalStorage(),
46 finishedProposals: NewProposalStorage(),
47 }
48 for _, apply := range options {
49 apply(dao)
50 }
51 return dao
52}
53
54// ID returns DAO's unique identifier.
55func (dao CommonDAO) ID() uint64 {
56 return dao.id
57}
58
59// Slug returns DAO's URL slug.
60func (dao CommonDAO) Slug() string {
61 return dao.slug
62}
63
64// Name returns DAO's name.
65func (dao CommonDAO) Name() string {
66 return dao.name
67}
68
69// Description returns DAO's description.
70func (dao CommonDAO) Description() string {
71 return dao.description
72}
73
74// Path returns the full path to the DAO.
75// Paths are normally used when working with hierarchical
76// DAOs and is created by concatenating DAO slugs.
77func (dao CommonDAO) Path() string {
78 // NOTE: Path could be a value but there might be use cases where dynamic path is useful (?)
79 parent := dao.Parent()
80 if parent != nil {
81 prefix := parent.Path()
82 if prefix != "" {
83 return prefix + PathSeparator + dao.slug
84 }
85 }
86 return dao.slug
87}
88
89// Parent returns the parent DAO.
90// Null can be returned when DAO has no parent assigned.
91func (dao CommonDAO) Parent() *CommonDAO {
92 return dao.parent
93}
94
95// Children returns a list with the direct DAO children.
96// Each item in the list is a reference to a CommonDAO instance.
97func (dao CommonDAO) Children() list.IList {
98 return dao.children
99}
100
101// TopParent returns the topmost parent DAO.
102// The top parent is the root of the DAO tree.
103func (dao *CommonDAO) TopParent() *CommonDAO {
104 parent := dao.Parent()
105 if parent != nil {
106 return parent.TopParent()
107 }
108 return dao
109}
110
111// Members returns the list of DAO members.
112func (dao CommonDAO) Members() MemberStorage {
113 return dao.members
114}
115
116// ActiveProposals returns active DAO proposals.
117func (dao CommonDAO) ActiveProposals() ProposalStorage {
118 return dao.activeProposals
119}
120
121// FinishedProposalsi returns finished DAO proposals.
122func (dao CommonDAO) FinishedProposals() ProposalStorage {
123 return dao.finishedProposals
124}
125
126// IsDeleted returns true when DAO has been soft deleted.
127func (dao CommonDAO) IsDeleted() bool {
128 return dao.deleted
129}
130
131// SetDeleted changes DAO's soft delete flag.
132func (dao *CommonDAO) SetDeleted(deleted bool) {
133 dao.deleted = deleted
134}
135
136// Propose creates a new DAO proposal.
137func (dao *CommonDAO) Propose(creator address, d ProposalDefinition) (*Proposal, error) {
138 id, ok := dao.genID.TryNext()
139 if !ok {
140 return nil, ErrOverflow
141 }
142
143 p, err := NewProposal(uint64(id), creator, d)
144 if err != nil {
145 return nil, err
146 }
147
148 dao.activeProposals.Add(p)
149 return p, nil
150}
151
152// MustPropose creates a new DAO proposal or panics on error.
153func (dao *CommonDAO) MustPropose(creator address, d ProposalDefinition) *Proposal {
154 p, err := dao.Propose(creator, d)
155 if err != nil {
156 panic(err)
157 }
158 return p
159}
160
161// GetProposal returns a proposal or nil when proposal is not found.
162func (dao CommonDAO) GetProposal(proposalID uint64) *Proposal {
163 p := dao.activeProposals.Get(proposalID)
164 if p != nil {
165 return p
166 }
167 return dao.finishedProposals.Get(proposalID)
168}
169
170// Withdraw withdraws a proposal that has no votes.
171// Only proposals without votes can be withdrawn, and once
172// withdrawn they are considered finished.
173func (dao *CommonDAO) Withdraw(proposalID uint64) error {
174 p := dao.activeProposals.Get(proposalID)
175 if p == nil {
176 return ErrProposalNotFound
177 }
178
179 if p.VotingRecord().Size() > 0 {
180 return ErrWithdrawalNotAllowed
181 }
182
183 p.status = StatusWithdrawn
184 dao.activeProposals.Remove(p.id)
185 dao.finishedProposals.Add(p)
186 return nil
187}
188
189// Vote submits a new vote for a proposal.
190//
191// By default votes are only allowed to members of the DAO when the proposal is active,
192// and within the voting period. No votes are allowed once the voting deadline passes.
193// DAO deadline checks can optionally be disabled using the `DisableVotingDeadlineCheck` option.
194func (dao *CommonDAO) Vote(member address, proposalID uint64, c VoteChoice, reason string) error {
195 if !dao.Members().Has(member) {
196 return ErrNotMember
197 }
198
199 p := dao.activeProposals.Get(proposalID)
200 if p == nil {
201 return ErrProposalNotFound
202 }
203
204 if !dao.disableVotingDeadlineCheck && p.HasVotingDeadlinePassed() {
205 return ErrVotingDeadlinePassed
206 }
207
208 if !p.IsVoteChoiceValid(c) {
209 return ErrInvalidVoteChoice
210 }
211
212 p.record.AddVote(Vote{
213 Address: member,
214 Choice: c,
215 Reason: reason,
216 })
217 return nil
218}
219
220// Execute executes a proposal.
221//
222// By default active proposals can only be executed after their voting deadline passes.
223// DAO deadline checks can optionally be disabled using the `DisableVotingDeadlineCheck` option.
224func (dao *CommonDAO) Execute(proposalID uint64) error {
225 p := dao.activeProposals.Get(proposalID)
226 if p == nil {
227 return ErrProposalNotFound
228 }
229
230 // Proposal must be active or have passed to be executed
231 if p.status != StatusActive && p.status != StatusPassed {
232 return ErrExecutionNotAllowed
233 }
234
235 // Execution must be done after voting deadline
236 if !dao.disableVotingDeadlineCheck && !p.HasVotingDeadlinePassed() {
237 return ErrVotingDeadlineNotMet
238 }
239
240 // IMPORTANT, from this point on, any error is going to result
241 // in a proposal failure and execute will succeed.
242
243 // Validate proposal before execution
244 err := p.Validate()
245
246 // Tally votes and update proposal status to "passed" or "rejected"
247 if err == nil {
248 err = p.Tally(dao.Members())
249 if err == nil && p.Status() == StatusRejected {
250 // Don't try to execute proposal if it's been rejected
251 return nil
252 }
253 }
254
255 // Execute proposal only if it's executable
256 if err == nil {
257 if e, ok := p.Definition().(Executable); ok {
258 if fn := e.Executor(); fn != nil {
259 err = fn(cross)
260 }
261 }
262 }
263
264 // Proposal fails if there is any error during validation and execution process
265 if err != nil {
266 p.status = StatusFailed
267 p.statusReason = err.Error()
268 } else {
269 p.status = StatusExecuted
270 }
271
272 // Whichever the outcome of the validation, tallying
273 // and execution consider the proposal finished.
274 dao.activeProposals.Remove(p.id)
275 dao.finishedProposals.Add(p)
276 return nil
277}