Search Apps Documentation Source Content File Folder Download Copy Actions Download

proposal.gno

8.69 Kb · 300 lines
  1package commondao
  2
  3import (
  4	"errors"
  5	"time"
  6
  7	"gno.land/p/nt/avl/v0"
  8)
  9
 10const (
 11	StatusActive    ProposalStatus = "active"
 12	StatusPassed                   = "passed"
 13	StatusRejected                 = "rejected"
 14	StatusExecuted                 = "executed"
 15	StatusFailed                   = "failed"
 16	StatusWithdrawn                = "withdrawn"
 17)
 18
 19const (
 20	ChoiceNone       VoteChoice = ""
 21	ChoiceYes                   = "YES"
 22	ChoiceNo                    = "NO"
 23	ChoiceNoWithVeto            = "NO WITH VETO"
 24	ChoiceAbstain               = "ABSTAIN"
 25)
 26
 27const (
 28	QuorumOneThird     float64 = 0.33 // percentage, checked as >= than quorum
 29	QuorumMoreThanHalf         = 0.51
 30	QuorumTwoThirds            = 0.66
 31	QuorumThreeFourths         = 0.75
 32	QuorumFull                 = 1
 33)
 34
 35// MaxCustomVoteChoices defines the maximum number of custom
 36// vote choices that a proposal definition can define.
 37const MaxCustomVoteChoices = 10
 38
 39var (
 40	ErrInvalidCreatorAddress      = errors.New("invalid proposal creator address")
 41	ErrMaxCustomVoteChoices       = errors.New("max number of custom vote choices exceeded")
 42	ErrProposalDefinitionRequired = errors.New("proposal definition is required")
 43	ErrNoQuorum                   = errors.New("no quorum")
 44	ErrStatusIsNotActive          = errors.New("proposal status is not active")
 45)
 46
 47type (
 48	// ProposalStatus defines a type for different proposal states.
 49	ProposalStatus string
 50
 51	// VoteChoice defines a type for proposal vote choices.
 52	VoteChoice string
 53
 54	// ExecFunc defines a type for functions that executes proposals.
 55	ExecFunc func(realm) error
 56
 57	// Proposal defines a DAO proposal.
 58	Proposal struct {
 59		id             uint64
 60		status         ProposalStatus
 61		definition     ProposalDefinition
 62		creator        address
 63		record         *VotingRecord // TODO: Add support for multiple voting records
 64		statusReason   string
 65		voteChoices    *avl.Tree // string(VoteChoice) -> struct{}
 66		votingDeadline time.Time
 67		createdAt      time.Time
 68	}
 69
 70	// ProposalDefinition defines an interface for custom proposal definitions.
 71	// These definitions define proposal content and behavior, they esentially
 72	// allow the definition for different proposal types.
 73	ProposalDefinition interface {
 74		// Title returns the proposal title.
 75		Title() string
 76
 77		// Body returns proposal's body.
 78		// It usually contains description or values that are specific to the proposal,
 79		// like a description of the proposal's motivation or the list of values that
 80		// would be applied when the proposal is approved.
 81		Body() string
 82
 83		// VotingPeriod returns the period where votes are allowed after proposal creation.
 84		// It is used to calculate the voting deadline from the proposal's creationd date.
 85		VotingPeriod() time.Duration
 86
 87		// Tally counts the number of votes and verifies if proposal passes.
 88		// It receives a voting context containing a readonly record with the votes
 89		// that has been submitted for the proposal and also the list of DAO members.
 90		Tally(VotingContext) (passes bool, _ error)
 91	}
 92
 93	// Validable defines an interface for proposal definitions that require state validation.
 94	// Validation is done before execution and normally also during proposal rendering.
 95	Validable interface {
 96		// Validate validates that the proposal is valid for the current state.
 97		Validate() error
 98	}
 99
100	// Executable defines an interface for proposal definitions that modify state on approval.
101	// Once proposals are executed they are archived and considered finished.
102	Executable interface {
103		// Executor returns a function to execute the proposal.
104		Executor() ExecFunc
105	}
106
107	// CustomizableVoteChoices defines an interface for proposal definitions that want
108	// to customize the list of allowed voting choices.
109	CustomizableVoteChoices interface {
110		// CustomVoteChoices returns a list of valid voting choices.
111		// Choices are considered valid only when there are at least two possible choices
112		// otherwise proposal defaults to using YES, NO and ABSTAIN as valid choices.
113		CustomVoteChoices() []VoteChoice
114	}
115)
116
117// MustValidate validates that a proposal is valid for the current state or panics on error.
118func MustValidate(v Validable) {
119	if v == nil {
120		panic("validable proposal definition is nil")
121	}
122
123	if err := v.Validate(); err != nil {
124		panic(err)
125	}
126}
127
128// MustExecute executes an executable proposal or panics on error.
129func MustExecute(e Executable) {
130	if e == nil {
131		panic("executable proposal definition is nil")
132	}
133
134	fn := e.Executor()
135	if fn == nil {
136		return
137	}
138
139	if err := fn(cross); err != nil {
140		panic(err)
141	}
142}
143
144// NewProposal creates a new DAO proposal.
145func NewProposal(id uint64, creator address, d ProposalDefinition) (*Proposal, error) {
146	if d == nil {
147		return nil, ErrProposalDefinitionRequired
148	}
149
150	if !creator.IsValid() {
151		return nil, ErrInvalidCreatorAddress
152	}
153
154	now := time.Now()
155	p := &Proposal{
156		id:             id,
157		status:         StatusActive,
158		definition:     d,
159		creator:        creator,
160		record:         &VotingRecord{},
161		voteChoices:    avl.NewTree(),
162		votingDeadline: now.Add(d.VotingPeriod()),
163		createdAt:      now,
164	}
165
166	if v, ok := d.(CustomizableVoteChoices); ok {
167		choices := v.CustomVoteChoices()
168		if len(choices) > MaxCustomVoteChoices {
169			return nil, ErrMaxCustomVoteChoices
170		}
171
172		for _, c := range choices {
173			p.voteChoices.Set(string(c), struct{}{})
174		}
175	}
176
177	// Use default voting choices when the definition returns none or a single vote choice
178	if p.voteChoices.Size() < 2 {
179		p.voteChoices.Set(string(ChoiceYes), struct{}{})
180		p.voteChoices.Set(string(ChoiceNo), struct{}{})
181		p.voteChoices.Set(string(ChoiceAbstain), struct{}{})
182	}
183	return p, nil
184}
185
186// ID returns the unique proposal identifies.
187func (p Proposal) ID() uint64 {
188	return p.id
189}
190
191// Definition returns the proposal definition.
192// Proposal definitions define proposal content and behavior.
193func (p Proposal) Definition() ProposalDefinition {
194	return p.definition
195}
196
197// Status returns the current proposal status.
198func (p Proposal) Status() ProposalStatus {
199	return p.status
200}
201
202// Creator returns the address of the account that created the proposal.
203func (p Proposal) Creator() address {
204	return p.creator
205}
206
207// CreatedAt returns the time that proposal was created.
208func (p Proposal) CreatedAt() time.Time {
209	return p.createdAt
210}
211
212// VotingRecord returns a record that contains all the votes submitted for the proposal.
213func (p Proposal) VotingRecord() *VotingRecord {
214	return p.record
215}
216
217// StatusReason returns an optional reason that lead to the current proposal status.
218// Reason is mostyl useful when a proposal fails.
219func (p Proposal) StatusReason() string {
220	return p.statusReason
221}
222
223// VotingDeadline returns the deadline after which no more votes should be allowed.
224func (p Proposal) VotingDeadline() time.Time {
225	return p.votingDeadline
226}
227
228// VoteChoices returns the list of vote choices allowed for the proposal.
229func (p Proposal) VoteChoices() []VoteChoice {
230	choices := make([]VoteChoice, 0, p.voteChoices.Size())
231	p.voteChoices.Iterate("", "", func(c string, _ any) bool {
232		choices = append(choices, VoteChoice(c))
233		return false
234	})
235	return choices
236}
237
238// HasVotingDeadlinePassed checks if the voting deadline has been met.
239func (p Proposal) HasVotingDeadlinePassed() bool {
240	return !time.Now().Before(p.VotingDeadline())
241}
242
243// Validate validates that a proposal is valid for the current state.
244// Validation is done when proposal status is active and when the definition supports validation.
245func (p Proposal) Validate() error {
246	if p.status != StatusActive {
247		return nil
248	}
249
250	if v, ok := p.definition.(Validable); ok {
251		return v.Validate()
252	}
253	return nil
254}
255
256// IsVoteChoiceValid checks if a vote choice is valid for the proposal.
257func (p Proposal) IsVoteChoiceValid(c VoteChoice) bool {
258	return p.voteChoices.Has(string(c))
259}
260
261// Tally counts votes and updates proposal status with the current outcome.
262// Proposal status is updated to "passed" when proposal is approved
263// or to "rejected" if proposal doesn't pass.
264func (p *Proposal) Tally(members MemberStorage) error {
265	if p.status != StatusActive {
266		return ErrStatusIsNotActive
267	}
268
269	ctx := MustNewVotingContext(p.VotingRecord(), members)
270	passes, err := p.Definition().Tally(ctx)
271	if err != nil {
272		return err
273	}
274
275	if passes {
276		p.status = StatusPassed
277	} else {
278		p.status = StatusRejected
279	}
280	return nil
281}
282
283// IsQuorumReached checks if a participation quorum is reach.
284func IsQuorumReached(quorum float64, r ReadonlyVotingRecord, members ReadonlyMemberStorage) bool {
285	if members.Size() <= 0 || quorum <= 0 {
286		return false
287	}
288
289	var totalCount int
290	r.IterateVotesCount(func(c VoteChoice, voteCount int) bool {
291		// Don't count explicit abstentions or invalid votes
292		if c != ChoiceNone && c != ChoiceAbstain {
293			totalCount += r.VoteCount(c)
294		}
295		return false
296	})
297
298	percentage := float64(totalCount) / float64(members.Size())
299	return percentage >= quorum
300}