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}