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

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