Search Apps Documentation Source Content File Folder Download Copy Actions Download

proposal.gno

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