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

governor.gno

6.23 Kb · 229 lines
  1// Package governor is a simplified on-chain governance realm inspired by
  2// OpenZeppelin's Governor. Anyone may open a proposal; every address gets a
  3// single equally-weighted vote (1 address = 1 vote); a proposal succeeds when
  4// its voting deadline has passed and the "for" tally strictly beats "against".
  5package governor
  6
  7import (
  8	"strconv"
  9	"strings"
 10
 11	"chain"
 12	"chain/runtime"
 13	"chain/runtime/unsafe"
 14
 15	"gno.land/p/nt/avl/v0"
 16)
 17
 18// votingPeriod is the number of blocks a proposal stays open for voting.
 19const votingPeriod = int64(100)
 20
 21// Vote support values.
 22const (
 23	VoteAgainst = 0
 24	VoteFor     = 1
 25	VoteAbstain = 2
 26)
 27
 28// Proposal state values.
 29const (
 30	StateActive    = "Active"
 31	StateSucceeded = "Succeeded"
 32	StateDefeated  = "Defeated"
 33	StateExecuted  = "Executed"
 34)
 35
 36// Proposal is a single governance proposal.
 37type Proposal struct {
 38	ID             int64
 39	Proposer       address
 40	Description    string
 41	SnapshotHeight int64
 42	Deadline       int64
 43	For            int64
 44	Against        int64
 45	Abstain        int64
 46	Executed       bool
 47	votes          *avl.Tree // voter address (string) -> support (int)
 48}
 49
 50var (
 51	proposals  = avl.NewTree() // id (zero-padded string) -> *Proposal
 52	nextID     = int64(1)
 53	totalCount int64
 54)
 55
 56// Propose opens a new proposal and returns its id. The snapshot height and
 57// deadline are recorded from the current chain height.
 58func Propose(cur realm, description string) int64 {
 59	if strings.TrimSpace(description) == "" {
 60		panic("governor: empty description")
 61	}
 62	proposer := unsafe.PreviousRealm().Address()
 63	h := runtime.ChainHeight()
 64	id := nextID
 65	nextID++
 66	totalCount++
 67
 68	p := &Proposal{
 69		ID:             id,
 70		Proposer:       proposer,
 71		Description:    description,
 72		SnapshotHeight: h,
 73		Deadline:       h + votingPeriod,
 74		votes:          avl.NewTree(),
 75	}
 76	proposals.Set(idKey(id), p)
 77
 78	chain.Emit(
 79		"ProposalCreated",
 80		"id", strconv.FormatInt(id, 10),
 81		"proposer", proposer.String(),
 82		"deadline", strconv.FormatInt(p.Deadline, 10),
 83	)
 84	return id
 85}
 86
 87// CastVote records one vote for the caller on proposal id. support is
 88// 0=against, 1=for, 2=abstain. One address may vote at most once per proposal,
 89// and voting is only allowed while the proposal is Active.
 90func CastVote(cur realm, id int64, support int) {
 91	p := mustGet(id)
 92	if computeState(p, runtime.ChainHeight()) != StateActive {
 93		panic("governor: voting closed")
 94	}
 95	if support < VoteAgainst || support > VoteAbstain {
 96		panic("governor: invalid support value")
 97	}
 98	voter := unsafe.PreviousRealm().Address()
 99	if p.votes.Has(voter.String()) {
100		panic("governor: already voted")
101	}
102	p.votes.Set(voter.String(), support)
103
104	switch support {
105	case VoteFor:
106		p.For++
107	case VoteAgainst:
108		p.Against++
109	default:
110		p.Abstain++
111	}
112
113	chain.Emit(
114		"VoteCast",
115		"id", strconv.FormatInt(id, 10),
116		"voter", voter.String(),
117		"support", strconv.Itoa(support),
118	)
119}
120
121// Execute marks a Succeeded proposal as executed. It panics unless the
122// proposal has reached the Succeeded state.
123func Execute(cur realm, id int64) {
124	p := mustGet(id)
125	if computeState(p, runtime.ChainHeight()) != StateSucceeded {
126		panic("governor: proposal not in Succeeded state")
127	}
128	p.Executed = true
129	chain.Emit("ProposalExecuted", "id", strconv.FormatInt(id, 10))
130}
131
132// State returns the current lifecycle state of proposal id.
133func State(id int64) string {
134	return computeState(mustGet(id), runtime.ChainHeight())
135}
136
137// computeState is the pure state machine: it decides the state of p at chain
138// height h. Kept free of globals so it is unit-testable.
139func computeState(p *Proposal, h int64) string {
140	if p.Executed {
141		return StateExecuted
142	}
143	if h < p.Deadline {
144		return StateActive
145	}
146	if p.For > p.Against {
147		return StateSucceeded
148	}
149	return StateDefeated
150}
151
152func mustGet(id int64) *Proposal {
153	key := idKey(id)
154	if !proposals.Has(key) {
155		panic("governor: unknown proposal " + strconv.FormatInt(id, 10))
156	}
157	return proposals.Get(key).(*Proposal)
158}
159
160// idKey returns a zero-padded, lexicographically-sortable key for an id so the
161// avl tree iterates proposals in numeric order.
162func idKey(id int64) string {
163	s := strconv.FormatInt(id, 10)
164	const width = 12
165	if len(s) >= width {
166		return s
167	}
168	return strings.Repeat("0", width-len(s)) + s
169}
170
171// bar renders a proportional ▓░ progress bar of fixed width.
172func bar(value, total int64, width int) string {
173	if width <= 0 {
174		return ""
175	}
176	filled := 0
177	if total > 0 {
178		filled = int((value*int64(width) + total/2) / total)
179		if filled > width {
180			filled = width
181		}
182	}
183	return strings.Repeat("▓", filled) + strings.Repeat("░", width-filled)
184}
185
186// Render lists all proposals with their tallies and states as Markdown.
187func Render(path string) string {
188	var b strings.Builder
189	b.WriteString("# 🏛️ Governor\n\n")
190	b.WriteString("Simplified on-chain governance — 1 address = 1 vote, ")
191	b.WriteString("voting period of " + strconv.FormatInt(votingPeriod, 10) + " blocks.\n\n")
192
193	h := runtime.ChainHeight()
194	b.WriteString("Current chain height: **" + strconv.FormatInt(h, 10) + "**\n\n")
195
196	if totalCount == 0 {
197		b.WriteString("_No proposals yet. Call `Propose` to create one._\n")
198		return b.String()
199	}
200
201	b.WriteString("Total proposals: **" + strconv.FormatInt(totalCount, 10) + "**\n\n")
202
203	proposals.Iterate("", "", func(_ string, v interface{}) bool {
204		p := v.(*Proposal)
205		state := computeState(p, h)
206		total := p.For + p.Against + p.Abstain
207
208		b.WriteString("---\n\n")
209		b.WriteString("## #" + strconv.FormatInt(p.ID, 10) + " · " + state + "\n\n")
210		b.WriteString("> " + p.Description + "\n\n")
211		b.WriteString("- Proposer: `" + p.Proposer.String() + "`\n")
212		b.WriteString("- Snapshot height: " + strconv.FormatInt(p.SnapshotHeight, 10) + "\n")
213		b.WriteString("- Deadline height: " + strconv.FormatInt(p.Deadline, 10))
214		if state == StateActive {
215			b.WriteString(" (" + strconv.FormatInt(p.Deadline-h, 10) + " blocks left)")
216		}
217		b.WriteString("\n\n")
218
219		b.WriteString("| Choice | Votes | Tally |\n")
220		b.WriteString("|---|---|---|\n")
221		b.WriteString("| For | " + strconv.FormatInt(p.For, 10) + " | `" + bar(p.For, total, 20) + "` |\n")
222		b.WriteString("| Against | " + strconv.FormatInt(p.Against, 10) + " | `" + bar(p.Against, total, 20) + "` |\n")
223		b.WriteString("| Abstain | " + strconv.FormatInt(p.Abstain, 10) + " | `" + bar(p.Abstain, total, 20) + "` |\n\n")
224
225		return false
226	})
227
228	return b.String()
229}