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}