proposal.gno

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