Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

proposal.gno

10.26 Kb · 282 lines
  1package commondao
  2
  3import (
  4	"errors"
  5	"time"
  6
  7	"gno.land/p/nt/addrset/v0"
  8)
  9
 10const (
 11	StatusActive    ProposalStatus = "active"
 12	StatusPassed    ProposalStatus = "passed"
 13	StatusDismissed ProposalStatus = "dismissed"
 14	StatusExecuted  ProposalStatus = "executed"
 15	StatusFailed    ProposalStatus = "failed"
 16	StatusWithdrawn ProposalStatus = "withdrawn"
 17)
 18
 19// Vote choices, fixed by the Common DAO Spec's default voting rules.
 20const (
 21	ChoiceYes     VoteChoice = "YES"
 22	ChoiceNo      VoteChoice = "NO"
 23	ChoiceAbstain VoteChoice = "ABSTAIN"
 24)
 25
 26// Thresholds for the constitution's default Council voting rules.
 27const (
 28	// ThresholdSupermajority passes with "two thirds or more" of the
 29	// tally denominator. The default for Council decisions.
 30	ThresholdSupermajority Threshold = iota
 31
 32	// ThresholdSimpleMajority passes with "more than half" of the
 33	// tally denominator. The Constitution assigns it to specific
 34	// decisions, e.g. sub-DAO creation.
 35	ThresholdSimpleMajority
 36)
 37
 38// Outcomes of tallying a proposal under the default Council rules.
 39const (
 40	OutcomePending Outcome = iota
 41	OutcomePassed
 42	OutcomeDismissed
 43)
 44
 45var (
 46	ErrInvalidCreatorAddress      = errors.New("invalid proposal creator address")
 47	ErrInvalidVoterAddress        = errors.New("invalid voter address")
 48	ErrProposalDefinitionRequired = errors.New("proposal definition is required")
 49	ErrStatusIsNotActive          = errors.New("proposal status is not active")
 50)
 51
 52type (
 53	// ProposalStatus defines a type for different proposal states.
 54	ProposalStatus string
 55
 56	// VoteChoice defines a type for proposal vote choices.
 57	VoteChoice string
 58
 59	// Threshold defines a type for the default tally thresholds.
 60	Threshold int
 61
 62	// Outcome defines a type for default tally outcomes.
 63	Outcome int
 64
 65	// ExecFunc defines a type for functions that execute proposals.
 66	//
 67	// The leading int makes ExecFunc non-crossing: the host calls it
 68	// directly (no cross), so the executor holds no realm cur of its own —
 69	// only the realm argument, a DAO-scoped sub-identity the host mints and
 70	// passes. Fund-moving executors send through that sub (e.g. banker
 71	// RealmSend), which is terminal and bounded to one DAO address;
 72	// executors that move no funds ignore it. The int is unused.
 73	//
 74	// Authority note: the sub is a least-authority DEFAULT, not a sandbox.
 75	// An executor is trusted realm code; because the sub is the executor's
 76	// only current realm value, it could regain the host realm's primary
 77	// authority via an explicit cross(sub) into a crossing function. That is
 78	// a visible, auditable call the reference realm's executors never make,
 79	// so their blast radius is one treasury — but a realm that runs
 80	// untrusted or user-registered executors gets no such guarantee. See ADR
 81	// pr6012_commondao_exec_scope.
 82	//
 83	// The sharper hazard for such a realm is not cross(sub) but the banker:
 84	// an executor can mint banker.NewBanker(BankerTypeRealmSend, sub) and
 85	// simply RETAIN it. Authorization happens at construction only, and the
 86	// banker holds no realm reference, so it persists across transactions
 87	// even though the sub itself cannot — a permanent, unrevocable
 88	// capability over that DAO's address, spendable later with no proposal.
 89	// It also bypasses any check the host performs before spending (a
 90	// frozen flag, a pause switch), because it reaches the bank keeper
 91	// without re-entering host code. Passing the sub to an executor whose
 92	// code the DAO has not vetted is therefore an irrevocable grant of that
 93	// DAO's treasury, not a scoped loan of it.
 94	ExecFunc func(int, realm) error
 95
 96	// Proposal defines a DAO proposal.
 97	Proposal struct {
 98		id             uint64
 99		status         ProposalStatus
100		definition     ProposalDefinition
101		creator        address
102		record         *VotingRecord
103		electorate     *addrset.Set // council snapshot taken at Propose
104		statusReason   string
105		votingDeadline time.Time
106		createdAt      time.Time
107	}
108
109	// ProposalDefinition defines an interface for custom proposal definitions.
110	// These definitions define proposal content and behavior, essentially
111	// allowing the definition of different proposal types.
112	ProposalDefinition interface {
113		// Title returns the proposal title.
114		Title() string
115
116		// Body returns proposal's body.
117		// It usually contains description or values that are specific to the proposal,
118		// like a description of the proposal's motivation or the list of values that
119		// would be applied when the proposal is approved.
120		Body() string
121
122		// VotingPeriod returns the period where votes are allowed after proposal creation.
123		// It is used to calculate the voting deadline from the proposal's creation date.
124		VotingPeriod() time.Duration
125
126		// Threshold returns the tally threshold for passing the proposal.
127		// Proposals are decided by the constitution's default Council voting
128		// rules: re-evaluated after every recorded vote, they can pass or be
129		// dismissed before their voting deadline.
130		//
131		// Threshold is read on every Vote (for early passage) AND again in
132		// the post-deadline re-tally inside Execute. Return a CONSTANT value:
133		// a threshold that loosens over a proposal's lifetime can let the
134		// deadline re-tally pass with fewer YES votes than voters faced when
135		// they cast under the stricter earlier value. A changing threshold is
136		// honored, but the definition author owns that consequence.
137		Threshold() Threshold
138	}
139
140	// ProposalKind defines an interface for proposal kinds: named factories
141	// for proposal definitions, registered per DAO. A kind is both the
142	// registry key (Name) and the factory (New) for one proposal type, and
143	// a DAO accepts proposals of exactly the kinds registered on it
144	// (CommonDAO.RegisterKind).
145	ProposalKind interface {
146		// Name returns the kind name used as registry key, e.g. "treasury-spend".
147		Name() string
148
149		// New validates args and builds the proposal definition. Propose
150		// passes a ReadonlyCommonDAO view of the host DAO, so New is a
151		// pure factory that cannot mutate the host or its tree before the
152		// vote; proposal targets and parameters come via args. A kind that
153		// must mutate state on execution receives the target *CommonDAO
154		// through args (which only trusted callers can populate), captures
155		// it, and mutates in its Executor. The returned definition's
156		// instance data is frozen at Propose like any proposal definition.
157		New(dao ReadonlyCommonDAO, args any) (ProposalDefinition, error)
158	}
159
160	// CapExempt defines an interface for proposal definitions that are not
161	// counted against the DAO's active proposals cap. Exempt definitions are
162	// instead bounded to one active proposal per creator, so that proposals
163	// which remove members (and therefore must never be blockable by a full
164	// cap) stay bounded.
165	CapExempt interface {
166		// CapExempt marks the definition as exempt.
167		CapExempt()
168	}
169
170	// Validable defines an interface for proposal definitions that require state validation.
171	// Validation is done before execution and normally also during proposal rendering.
172	Validable interface {
173		// Validate validates that the proposal is valid for the current state.
174		Validate() error
175	}
176
177	// Executable defines an interface for proposal definitions that modify state on approval.
178	// Once proposals are executed they are archived and considered finished.
179	Executable interface {
180		// Executor returns a function to execute the proposal.
181		Executor() ExecFunc
182	}
183)
184
185// newProposal creates a new DAO proposal.
186//
187// The proposal is created with an empty electorate; Propose populates it
188// with the council snapshot.
189func newProposal(id uint64, creator address, d ProposalDefinition) (*Proposal, error) {
190	if !creator.IsValid() {
191		return nil, ErrInvalidCreatorAddress
192	}
193
194	now := time.Now()
195	return &Proposal{
196		id:             id,
197		status:         StatusActive,
198		definition:     d,
199		creator:        creator,
200		record:         &VotingRecord{},
201		electorate:     &addrset.Set{},
202		votingDeadline: now.Add(d.VotingPeriod()),
203		createdAt:      now,
204	}, nil
205}
206
207// ID returns the unique proposal identifier.
208func (p Proposal) ID() uint64 {
209	return p.id
210}
211
212// Definition returns the proposal definition.
213// Proposal definitions define proposal content and behavior.
214func (p Proposal) Definition() ProposalDefinition {
215	return p.definition
216}
217
218// Status returns the current proposal status.
219func (p Proposal) Status() ProposalStatus {
220	return p.status
221}
222
223// Creator returns the address of the account that created the proposal.
224func (p Proposal) Creator() address {
225	return p.creator
226}
227
228// CreatedAt returns the time that proposal was created.
229func (p Proposal) CreatedAt() time.Time {
230	return p.createdAt
231}
232
233// VotingRecord returns a read only record with the votes submitted for
234// the proposal. Votes are recorded through CommonDAO.Vote only.
235func (p Proposal) VotingRecord() ReadonlyVotingRecord {
236	return p.record.Readonly()
237}
238
239// Electorate returns the proposal's electorate: a read only view of the
240// council snapshot taken when the proposal was created. Members added to
241// the council afterwards vote on the next proposal; members removed or
242// resigned afterwards remain in the electorate (their silence counts
243// against passage).
244func (p Proposal) Electorate() *addrset.ReadonlySet {
245	return p.electorate.Readonly()
246}
247
248// StatusReason returns an optional reason that led to the current proposal status.
249// Reason is mostly useful when a proposal fails.
250func (p Proposal) StatusReason() string {
251	return p.statusReason
252}
253
254// VotingDeadline returns the deadline after which no more votes should be allowed.
255func (p Proposal) VotingDeadline() time.Time {
256	return p.votingDeadline
257}
258
259// HasVotingDeadlinePassed checks if the voting deadline has been met.
260func (p Proposal) HasVotingDeadlinePassed() bool {
261	return !time.Now().Before(p.VotingDeadline())
262}
263
264// Validate validates that a proposal is valid for the current state.
265// Validation is done when the proposal can still be executed (status is
266// active or passed) and when the definition supports validation.
267func (p Proposal) Validate() error {
268	if p.status != StatusActive && p.status != StatusPassed {
269		return nil
270	}
271
272	if v, ok := p.definition.(Validable); ok {
273		return v.Validate()
274	}
275	return nil
276}
277
278// ExpectedOutcome returns the outcome the proposal would have if it were
279// decided with the votes submitted so far. Useful for rendering.
280func (p Proposal) ExpectedOutcome() Outcome {
281	return TallyDefault(p.record.Readonly(), p.Electorate(), p.definition.Threshold())
282}