commondao.gno
7.54 Kb · 282 lines
1package commondao
2
3import (
4 "errors"
5
6 "gno.land/p/nt/bptree/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.
224//
225// rlm is the realm authority threaded into the executor via cross(rlm).
226// Execute itself is not a crossing function (rlm sits in a non-first
227// parameter slot) because /p/ production code cannot declare crossing
228// functions — callers pass `cur` directly as the rlm argument.
229func (dao *CommonDAO) Execute(proposalID uint64, rlm realm) error {
230 p := dao.activeProposals.Get(proposalID)
231 if p == nil {
232 return ErrProposalNotFound
233 }
234
235 // Proposal must be active or have passed to be executed
236 if p.status != StatusActive && p.status != StatusPassed {
237 return ErrExecutionNotAllowed
238 }
239
240 // Execution must be done after voting deadline
241 if !dao.disableVotingDeadlineCheck && !p.HasVotingDeadlinePassed() {
242 return ErrVotingDeadlineNotMet
243 }
244
245 // IMPORTANT, from this point on, any error is going to result
246 // in a proposal failure and execute will succeed.
247
248 // Validate proposal before execution
249 err := p.Validate()
250
251 // Tally votes and update proposal status to "passed" or "rejected"
252 if err == nil {
253 err = p.Tally(dao.Members())
254 if err == nil && p.Status() == StatusRejected {
255 // Don't try to execute proposal if it's been rejected
256 return nil
257 }
258 }
259
260 // Execute proposal only if it's executable
261 if err == nil {
262 if e, ok := p.Definition().(Executable); ok {
263 if fn := e.Executor(); fn != nil {
264 err = fn(cross(rlm))
265 }
266 }
267 }
268
269 // Proposal fails if there is any error during validation and execution process
270 if err != nil {
271 p.status = StatusFailed
272 p.statusReason = err.Error()
273 } else {
274 p.status = StatusExecuted
275 }
276
277 // Whichever the outcome of the validation, tallying
278 // and execution consider the proposal finished.
279 dao.activeProposals.Remove(p.id)
280 dao.finishedProposals.Add(p)
281 return nil
282}