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

proposals.gno

5.26 Kb · 177 lines
  1package daokit
  2
  3// Handles DAO governance through proposal voting and execution.
  4// They contain actions that run when voting requirements are satisfied.
  5//
  6// Common use cases: member management, treasury operations, rule updates.
  7
  8import (
  9	"chain/runtime"
 10	"errors"
 11	"time"
 12
 13	"gno.land/p/nt/seqid/v0"
 14	"gno.land/p/onbloc/json"
 15	"gno.land/p/samcrew/avl"
 16	"gno.land/p/samcrew/daocond"
 17)
 18
 19type ProposalStatus int
 20
 21const (
 22	ProposalStatusOpen     ProposalStatus = iota // Currently accepting votes
 23	ProposalStatusPassed                         // Met conditions, ready for execution
 24	ProposalStatusExecuted                       // Successfully executed
 25)
 26
 27func (s ProposalStatus) String() string {
 28	switch s {
 29	case ProposalStatusOpen:
 30		return "Open"
 31	case ProposalStatusPassed:
 32		return "Passed"
 33	case ProposalStatusExecuted:
 34		return "Executed"
 35	default:
 36		return "Unknown"
 37	}
 38}
 39
 40type Proposal struct {
 41	ID            seqid.ID
 42	Title         string
 43	Description   string
 44	CreatedAt     time.Time
 45	CreatedHeight int64
 46	ProposerID    string
 47
 48	Action     Action            // What to execute if passed
 49	Condition  daocond.Condition // Voting conditions for this proposal
 50	Status     ProposalStatus
 51	ExecutedAt time.Time
 52
 53	Ballot daocond.Ballot // All votes cast on this proposal
 54}
 55
 56type ProposalsStore struct {
 57	Tree  *avl.Tree // int -> Proposal
 58	genID seqid.ID
 59}
 60
 61// Data needed to create a new proposal.
 62type ProposalRequest struct {
 63	Title       string
 64	Description string
 65	Action      Action
 66}
 67
 68func NewProposalsStore() *ProposalsStore {
 69	return &ProposalsStore{
 70		Tree: avl.NewTree(),
 71	}
 72}
 73
 74func (p *ProposalsStore) GetProposal(id uint64) *Proposal {
 75	value, ok := p.Tree.Get(seqid.ID(id).String())
 76	if !ok {
 77		return nil
 78	}
 79	proposal := value.(*Proposal)
 80	return proposal
 81}
 82
 83// Returns all proposals that match the given filter function.
 84func (p *ProposalsStore) GetProposals(filter func(Proposal) bool) *ProposalsStore {
 85	proposals := NewProposalsStore()
 86	p.Tree.Iterate("", "", func(key string, value interface{}) bool {
 87		prop, ok := value.(*Proposal)
 88		if !ok {
 89			panic(errors.New("unexpected invalid proposal type"))
 90		}
 91		if filter(*prop) {
 92			proposals.Tree.Set(key, prop)
 93		}
 94		return false
 95	})
 96	return proposals
 97}
 98
 99// Reports the status a proposal should be displayed as, without writing it.
100//
101// Reading must never move a proposal. Render is on the cross-realm DAO
102// interface, so anything it touches is reachable by any realm holding the
103// handle: every read path used to call UpdateStatus on each proposal it walked,
104// and a single Render flipped every condition-met proposal from Open to Passed —
105// which, back when Execute demanded Open, bricked it permanently.
106func (p *Proposal) DisplayStatus() ProposalStatus {
107	if p.Status == ProposalStatusOpen && p.Condition.Eval(p.Ballot) {
108		return ProposalStatusPassed
109	}
110	return p.Status
111}
112
113// Persists the status implied by the current votes: Open becomes Passed once the
114// condition is met.
115//
116// Nothing in this package calls it. Execute evaluates the condition itself and
117// the read paths use DisplayStatus, so a proposal that is never passed through
118// here stays Open until it is Executed. It exists for a realm that wants the
119// passed state on-chain rather than computed.
120//
121// SAFE ONLY BECAUSE Vote and Execute both key on Executed alone. When they
122// demanded Open, calling this was destructive twice over: it froze the ballot,
123// so support could not be withdrawn, and it made the proposal permanently
124// unexecutable. Do not narrow either of those checks without removing this.
125//
126// It is also a READ path hazard — do not call it while rendering. And once
127// persisted the status no longer tracks the votes: withdraw support afterwards
128// and this still reads Passed, though Execute re-evaluates the condition and
129// will refuse.
130func (p *Proposal) UpdateStatus() {
131	p.Status = p.DisplayStatus()
132}
133
134// Returns all proposals as a JSON string.
135func (p *ProposalsStore) GetProposalsJSON() string {
136	props := make([]*json.Node, 0, p.Tree.Size())
137	// XXX: pagination
138	p.Tree.Iterate("", "", func(key string, value interface{}) bool {
139		prop, ok := value.(*Proposal)
140		if !ok {
141			panic(errors.New("unexpected invalid proposal type"))
142		}
143		props = append(props, json.ObjectNode("", map[string]*json.Node{
144			"id":          json.NumberNode("", float64(prop.ID)),
145			"title":       json.StringNode("", prop.Title),
146			"description": json.StringNode("", prop.Description),
147			"proposer":    json.StringNode("", prop.ProposerID),
148			"status":      json.StringNode("", prop.DisplayStatus().String()),
149			"startHeight": json.NumberNode("", float64(prop.CreatedHeight)),
150			"signal":      json.NumberNode("", prop.Condition.Signal(prop.Ballot)),
151		}))
152		return false
153	})
154	bz, err := json.Marshal(json.ArrayNode("", props))
155	if err != nil {
156		panic(err)
157	}
158	return string(bz)
159}
160
161func (p *ProposalsStore) newProposal(proposer string, req ProposalRequest, condition daocond.Condition) *Proposal {
162	id := p.genID.Next()
163	proposal := &Proposal{
164		ID:            id,
165		Title:         req.Title,
166		Description:   req.Description,
167		ProposerID:    proposer,
168		Status:        ProposalStatusOpen,
169		Action:        req.Action,
170		Condition:     condition,
171		Ballot:        daocond.NewBallot(),
172		CreatedAt:     time.Now(),
173		CreatedHeight: runtime.ChainHeight(),
174	}
175	p.Tree.Set(id.String(), proposal)
176	return proposal
177}