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

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}