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.00 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/nym-moultest123/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}