1// Package governor is a simplified on-chain governance realm inspired by2// OpenZeppelin's Governor. Anyone may open a proposal; every address gets a3// single equally-weighted vote (1 address = 1 vote); a proposal succeeds when4// its voting deadline has passed and the "for" tally strictly beats "against".5package governor67import (8 "strconv"9 "strings"1011 "chain"12 "chain/runtime"13 "chain/runtime/unsafe"1415 "gno.land/p/nt/avl/v0"16)1718// votingPeriod is the number of blocks a proposal stays open for voting.19const votingPeriod = int64(100)2021// Vote support values.22const (23 VoteAgainst = 024 VoteFor = 125 VoteAbstain = 226)2728// Proposal state values.29const (30 StateActive = "Active"31 StateSucceeded = "Succeeded"32 StateDefeated = "Defeated"33 StateExecuted = "Executed"34)3536// Proposal is a single governance proposal.37type Proposal struct {38 ID int6439 Proposer address40 Description string41 SnapshotHeight int6442 Deadline int6443 For int6444 Against int6445 Abstain int6446 Executed bool47 votes *avl.Tree // voter address (string) -> support (int)48}4950var (51 proposals = avl.NewTree() // id (zero-padded string) -> *Proposal52 nextID = int64(1)53 totalCount int6454)5556// Propose opens a new proposal and returns its id. The snapshot height and57// 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 := nextID65 nextID++66 totalCount++6768 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)7778 chain.Emit(79 "ProposalCreated",80 "id", strconv.FormatInt(id, 10),81 "proposer", proposer.String(),82 "deadline", strconv.FormatInt(p.Deadline, 10),83 )84 return id85}8687// CastVote records one vote for the caller on proposal id. support is88// 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)103104 switch support {105 case VoteFor:106 p.For++107 case VoteAgainst:108 p.Against++109 default:110 p.Abstain++111 }112113 chain.Emit(114 "VoteCast",115 "id", strconv.FormatInt(id, 10),116 "voter", voter.String(),117 "support", strconv.Itoa(support),118 )119}120121// Execute marks a Succeeded proposal as executed. It panics unless the122// 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 = true129 chain.Emit("ProposalExecuted", "id", strconv.FormatInt(id, 10))130}131132// State returns the current lifecycle state of proposal id.133func State(id int64) string {134 return computeState(mustGet(id), runtime.ChainHeight())135}136137// computeState is the pure state machine: it decides the state of p at chain138// height h. Kept free of globals so it is unit-testable.139func computeState(p *Proposal, h int64) string {140 if p.Executed {141 return StateExecuted142 }143 if h < p.Deadline {144 return StateActive145 }146 if p.For > p.Against {147 return StateSucceeded148 }149 return StateDefeated150}151152func 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}159160// idKey returns a zero-padded, lexicographically-sortable key for an id so the161// avl tree iterates proposals in numeric order.162func idKey(id int64) string {163 s := strconv.FormatInt(id, 10)164 const width = 12165 if len(s) >= width {166 return s167 }168 return strings.Repeat("0", width-len(s)) + s169}170171// 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 := 0177 if total > 0 {178 filled = int((value*int64(width) + total/2) / total)179 if filled > width {180 filled = width181 }182 }183 return strings.Repeat("▓", filled) + strings.Repeat("░", width-filled)184}185186// Render lists all proposals with their tallies and states as Markdown.187func Render(path string) string {188 var b strings.Builder189 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")192193 h := runtime.ChainHeight()194 b.WriteString("Current chain height: **" + strconv.FormatInt(h, 10) + "**\n\n")195196 if totalCount == 0 {197 b.WriteString("_No proposals yet. Call `Propose` to create one._\n")198 return b.String()199 }200201 b.WriteString("Total proposals: **" + strconv.FormatInt(totalCount, 10) + "**\n\n")202203 proposals.Iterate("", "", func(_ string, v interface{}) bool {204 p := v.(*Proposal)205 state := computeState(p, h)206 total := p.For + p.Against + p.Abstain207208 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")218219 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")224225 return false226 })227228 return b.String()229}230CastVote(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64, support int)
Execute(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64)
Propose(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, description string) int64
Render(path string) string
State(id int64) string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.