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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

governor.gno

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