Search Apps Documentation Source Content File Folder Download Copy Actions Download

proposal.gno

9.00 Kb · 305 lines
  1package commondao
  2
  3import (
  4	"errors"
  5	"time"
  6
  7	"gno.land/p/nt/bptree/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    *bptree.BPTree // 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.
129//
130// rlm is the realm authority threaded into the executor via cross(rlm).
131// MustExecute itself is not a crossing function (rlm sits in a non-first
132// parameter slot) because /p/ production code cannot declare crossing
133// functions — callers pass `cur` directly as the rlm argument.
134func MustExecute(e Executable, rlm realm) {
135	if e == nil {
136		panic("executable proposal definition is nil")
137	}
138
139	fn := e.Executor()
140	if fn == nil {
141		return
142	}
143
144	if err := fn(cross(rlm)); err != nil {
145		panic(err)
146	}
147}
148
149// NewProposal creates a new DAO proposal.
150func NewProposal(id uint64, creator address, d ProposalDefinition) (*Proposal, error) {
151	if d == nil {
152		return nil, ErrProposalDefinitionRequired
153	}
154
155	if !creator.IsValid() {
156		return nil, ErrInvalidCreatorAddress
157	}
158
159	now := time.Now()
160	p := &Proposal{
161		id:             id,
162		status:         StatusActive,
163		definition:     d,
164		creator:        creator,
165		record:         &VotingRecord{},
166		voteChoices:    bptree.NewBPTree32(),
167		votingDeadline: now.Add(d.VotingPeriod()),
168		createdAt:      now,
169	}
170
171	if v, ok := d.(CustomizableVoteChoices); ok {
172		choices := v.CustomVoteChoices()
173		if len(choices) > MaxCustomVoteChoices {
174			return nil, ErrMaxCustomVoteChoices
175		}
176
177		for _, c := range choices {
178			p.voteChoices.Set(string(c), struct{}{})
179		}
180	}
181
182	// Use default voting choices when the definition returns none or a single vote choice
183	if p.voteChoices.Size() < 2 {
184		p.voteChoices.Set(string(ChoiceYes), struct{}{})
185		p.voteChoices.Set(string(ChoiceNo), struct{}{})
186		p.voteChoices.Set(string(ChoiceAbstain), struct{}{})
187	}
188	return p, nil
189}
190
191// ID returns the unique proposal identifies.
192func (p Proposal) ID() uint64 {
193	return p.id
194}
195
196// Definition returns the proposal definition.
197// Proposal definitions define proposal content and behavior.
198func (p Proposal) Definition() ProposalDefinition {
199	return p.definition
200}
201
202// Status returns the current proposal status.
203func (p Proposal) Status() ProposalStatus {
204	return p.status
205}
206
207// Creator returns the address of the account that created the proposal.
208func (p Proposal) Creator() address {
209	return p.creator
210}
211
212// CreatedAt returns the time that proposal was created.
213func (p Proposal) CreatedAt() time.Time {
214	return p.createdAt
215}
216
217// VotingRecord returns a record that contains all the votes submitted for the proposal.
218func (p Proposal) VotingRecord() *VotingRecord {
219	return p.record
220}
221
222// StatusReason returns an optional reason that lead to the current proposal status.
223// Reason is mostyl useful when a proposal fails.
224func (p Proposal) StatusReason() string {
225	return p.statusReason
226}
227
228// VotingDeadline returns the deadline after which no more votes should be allowed.
229func (p Proposal) VotingDeadline() time.Time {
230	return p.votingDeadline
231}
232
233// VoteChoices returns the list of vote choices allowed for the proposal.
234func (p Proposal) VoteChoices() []VoteChoice {
235	choices := make([]VoteChoice, 0, p.voteChoices.Size())
236	p.voteChoices.Iterate("", "", func(c string, _ any) bool {
237		choices = append(choices, VoteChoice(c))
238		return false
239	})
240	return choices
241}
242
243// HasVotingDeadlinePassed checks if the voting deadline has been met.
244func (p Proposal) HasVotingDeadlinePassed() bool {
245	return !time.Now().Before(p.VotingDeadline())
246}
247
248// Validate validates that a proposal is valid for the current state.
249// Validation is done when proposal status is active and when the definition supports validation.
250func (p Proposal) Validate() error {
251	if p.status != StatusActive {
252		return nil
253	}
254
255	if v, ok := p.definition.(Validable); ok {
256		return v.Validate()
257	}
258	return nil
259}
260
261// IsVoteChoiceValid checks if a vote choice is valid for the proposal.
262func (p Proposal) IsVoteChoiceValid(c VoteChoice) bool {
263	return p.voteChoices.Has(string(c))
264}
265
266// Tally counts votes and updates proposal status with the current outcome.
267// Proposal status is updated to "passed" when proposal is approved
268// or to "rejected" if proposal doesn't pass.
269func (p *Proposal) Tally(members MemberStorage) error {
270	if p.status != StatusActive {
271		return ErrStatusIsNotActive
272	}
273
274	ctx := MustNewVotingContext(p.VotingRecord(), members)
275	passes, err := p.Definition().Tally(ctx)
276	if err != nil {
277		return err
278	}
279
280	if passes {
281		p.status = StatusPassed
282	} else {
283		p.status = StatusRejected
284	}
285	return nil
286}
287
288// IsQuorumReached checks if a participation quorum is reach.
289func IsQuorumReached(quorum float64, r ReadonlyVotingRecord, members ReadonlyMemberStorage) bool {
290	if members.Size() <= 0 || quorum <= 0 {
291		return false
292	}
293
294	var totalCount int
295	r.IterateVotesCount(func(c VoteChoice, voteCount int) bool {
296		// Don't count explicit abstentions or invalid votes
297		if c != ChoiceNone && c != ChoiceAbstain {
298			totalCount += r.VoteCount(c)
299		}
300		return false
301	})
302
303	percentage := float64(totalCount) / float64(members.Size())
304	return percentage >= quorum
305}