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