Search Apps Documentation Source Content File Folder Download Copy Actions Download

proposal.gno

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