Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}