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.

gov.gno

10.83 Kb · 432 lines
  1// Package gov is the WIZ GovDAO treasury, token version 4.
  2//
  3// Seal on gno.land/r/nym-wizards001/drop sends two sleeves here:
  4// the incentive sleeve (7% of supply) and community development
  5// (3% plus the pro-rata remainder). A vote spends one sleeve.
  6// It cannot spend the other. Liquidity was sent to the address named
  7// in Seal, and this realm cannot move it.
  8//
  9// Voting weight is the signer's WIZ balance. One vote per address.
 10// The window is 7 days, the same duration as drop.VoteWindow.
 11// A spend passes when yes beats no and yes reaches drop.Quorum,
 12// 10% of the sealed airdrop. There is no withdraw and no listing vote.
 13package gov
 14
 15import (
 16	"chain"
 17	"chain/runtime/unsafe"
 18	"strconv"
 19	"strings"
 20	"time"
 21
 22	"gno.land/p/nt/avl/v0"
 23	"gno.land/p/nt/ufmt/v0"
 24
 25	"gno.land/r/nym-wizards001/drop"
 26)
 27
 28const (
 29	// KindSpend pays community WIZ to the named address.
 30	// The memo is optional and is not spent.
 31	KindSpend = 1
 32	// KindSignal stores a memo and moves nothing. Kind 2 is unused.
 33	KindSignal = 3
 34	// KindIncentive pays incentive WIZ to the named address.
 35	// The memo is optional and is not spent.
 36	KindIncentive = 4
 37
 38	// VoteWindow matches drop.VoteWindow. A ballot locks the voter's balance for this long.
 39	VoteWindow int64 = 7 * 24 * 60 * 60
 40	// maxMemo is the packed note: a title, a summary, and a change.
 41	maxMemo = 1200
 42
 43	notePrefix = "wizards-note/1\n"
 44	noteSplit  = "\n---\n"
 45)
 46
 47type proposal struct {
 48	kind      int
 49	proposer  address
 50	to        address
 51	tokenAmt  int64
 52	memo      string
 53	start     int64
 54	end       int64
 55	yes       int64
 56	no        int64
 57	executed  bool
 58	passed    bool
 59}
 60
 61type ballot struct {
 62	yes    bool
 63	weight int64
 64}
 65
 66var (
 67	treasury        address
 68	spentCommunity  int64
 69	spentIncentive  int64
 70	proposals       avl.Tree
 71	votes           avl.Tree
 72	nextProposal    int
 73)
 74
 75// Bind records this realm as the drop token's GovDAO.
 76// The transaction signer must be the token admin. One call, before Seal.
 77func Bind(cur realm) {
 78	mustUser(cur)
 79	mustNoPayment()
 80	if treasury.IsValid() {
 81		panic("bound")
 82	}
 83	// SetGov checks the signer before this realm records itself.
 84	// A rejected call must not leave treasury set.
 85	drop.SetGov(cross(cur))
 86	treasury = cur.Address()
 87	chain.Emit("Bind", "treasury", treasury.String())
 88}
 89
 90// Propose opens a vote. Claims on the token must already have started.
 91// The caller must hold WIZ. kind is KindSpend, KindSignal, or KindIncentive.
 92func Propose(cur realm, kind int, to address, tokenAmt int64, memo string) int {
 93	mustUser(cur)
 94	mustNoPayment()
 95	if !treasury.IsValid() {
 96		panic("gov")
 97	}
 98	if !drop.ClaimsStarted() {
 99		panic("claim not started")
100	}
101	caller := cur.Previous().Address()
102	if drop.BalanceOf(caller) <= 0 {
103		panic("proposer")
104	}
105	switch kind {
106	case KindSpend:
107		if len(memo) > maxMemo {
108			panic("proposal")
109		}
110		mustPayProposal(to, tokenAmt, CommunityLeft())
111	case KindIncentive:
112		if len(memo) > maxMemo {
113			panic("proposal")
114		}
115		mustPayProposal(to, tokenAmt, IncentiveLeft())
116	case KindSignal:
117		if to.IsValid() || tokenAmt != 0 {
118			panic("proposal")
119		}
120		if len(memo) == 0 || len(memo) > maxMemo {
121			panic("proposal")
122		}
123	default:
124		panic("proposal")
125	}
126	now := time.Now().Unix()
127	id := nextProposal
128	nextProposal++
129	proposals.Set(strconv.Itoa(id), &proposal{
130		kind:     kind,
131		proposer: caller,
132		to:       to,
133		tokenAmt: tokenAmt,
134		memo:     memo,
135		start:    now,
136		end:      now + VoteWindow,
137	})
138	toText := ""
139	if to.IsValid() {
140		toText = to.String()
141	}
142	chain.Emit(
143		"Propose",
144		"id", strconv.Itoa(id),
145		"kind", strconv.Itoa(kind),
146		"proposer", caller.String(),
147		"to", toText,
148		"tokenAmt", strconv.FormatInt(tokenAmt, 10),
149		"memo", memo,
150	)
151	return id
152}
153
154// Vote records the caller's current WIZ balance as yes or no.
155// One vote per address. That balance stays locked until this proposal ends.
156func Vote(cur realm, id int, yes bool) {
157	mustUser(cur)
158	mustNoPayment()
159	p := proposalOf(id)
160	now := time.Now().Unix()
161	if now < p.start || now >= p.end {
162		panic("voting")
163	}
164	caller := cur.Previous().Address()
165	key := voteKey(id, caller)
166	if votes.Has(key) {
167		panic("voted")
168	}
169	weight := drop.BalanceOf(caller)
170	if weight <= 0 {
171		panic("vote weight")
172	}
173	drop.NoteVote(cross(cur), p.end)
174	votes.Set(key, &ballot{yes: yes, weight: weight})
175	if yes {
176		p.yes += weight
177	} else {
178		p.no += weight
179	}
180	proposals.Set(strconv.Itoa(id), p)
181	yesText := "0"
182	if yes {
183		yesText = "1"
184	}
185	chain.Emit(
186		"Vote",
187		"id", strconv.Itoa(id),
188		"yes", yesText,
189		"voter", caller.String(),
190		"weight", strconv.FormatInt(weight, 10),
191	)
192}
193
194// Execute closes a finished proposal. A passed spend pays one sleeve.
195// A failed vote moves nothing. Anyone may call it.
196func Execute(cur realm, id int) {
197	mustCurrent(cur)
198	mustNoPayment()
199	p := proposalOf(id)
200	if p.executed {
201		panic("executed")
202	}
203	if time.Now().Unix() < p.end {
204		panic("voting")
205	}
206	passed := p.yes > p.no && p.yes >= drop.Quorum()
207	if passed && !executable(p) {
208		passed = false
209	}
210	p.executed = true
211	p.passed = passed
212	proposals.Set(strconv.Itoa(id), p)
213	if !passed {
214		chain.Emit("Reject", "id", strconv.Itoa(id))
215		return
216	}
217	switch p.kind {
218	case KindSpend:
219		drop.Transfer(cross(cur), p.to, p.tokenAmt)
220		spentCommunity += p.tokenAmt
221	case KindIncentive:
222		drop.Transfer(cross(cur), p.to, p.tokenAmt)
223		spentIncentive += p.tokenAmt
224	case KindSignal:
225	default:
226		panic("proposal")
227	}
228	chain.Emit("Execute", "id", strconv.Itoa(id), "kind", strconv.Itoa(p.kind))
229}
230
231func Treasury() int64 {
232	if !treasury.IsValid() {
233		return 0
234	}
235	return drop.BalanceOf(treasury)
236}
237
238// CommunityLeft is the community sleeve still available to a vote.
239func CommunityLeft() int64 {
240	return sleeveLeft(drop.CommunitySent(), spentCommunity)
241}
242
243// IncentiveLeft is the incentive sleeve still available to a vote.
244func IncentiveLeft() int64 {
245	return sleeveLeft(drop.IncentiveSent(), spentIncentive)
246}
247
248func TreasuryAddress() address { return treasury }
249
250func ProposalCount() int { return nextProposal }
251
252func ProposalKind(id int) int      { return proposalOf(id).kind }
253func ProposalYes(id int) int64     { return proposalOf(id).yes }
254func ProposalNo(id int) int64      { return proposalOf(id).no }
255func ProposalExecuted(id int) bool { return proposalOf(id).executed }
256func ProposalPassed(id int) bool   { return proposalOf(id).passed }
257func ProposalEnd(id int) int64     { return proposalOf(id).end }
258func ProposalTokenAmt(id int) int64 {
259	return proposalOf(id).tokenAmt
260}
261func ProposalTo(id int) address { return proposalOf(id).to }
262func ProposalMemo(id int) string {
263	return proposalOf(id).memo
264}
265func ProposalProposer(id int) address {
266	return proposalOf(id).proposer
267}
268
269// Voted reports whether voter has a ballot on id. A missing ballot is (false, false).
270func Voted(id int, voter address) (cast bool, yes bool) {
271	if id < 0 || id >= nextProposal || !voter.IsValid() {
272		return false, false
273	}
274	v := votes.Get(voteKey(id, voter))
275	if v == nil {
276		return false, false
277	}
278	b, ok := v.(*ballot)
279	if !ok || b == nil {
280		panic("vote")
281	}
282	return true, b.yes
283}
284
285// Render prints one proposal at "proposal/<id>". Other paths print the treasury.
286func Render(path string) string {
287	const proposalPrefix = "proposal/"
288	if len(path) >= len(proposalPrefix) && path[:len(proposalPrefix)] == proposalPrefix {
289		return renderProposal(path[len(proposalPrefix):])
290	}
291	out := "# WIZ GovDAO\n\n"
292	out += "* **Community development**: one sleeve, spent by a passed vote\n"
293	out += "* **Incentive**: a separate sleeve, spent by a passed vote\n"
294	out += "* **Liquidity**: not held here\n"
295	out += "* **Vote**: WIZ balance, 7 days, quorum 10% of the sealed airdrop\n"
296	out += "* **No withdraw**\n"
297	if treasury.IsValid() {
298		out += ufmt.Sprintf("\n* **Balance**: %d\n", Treasury())
299		out += ufmt.Sprintf("* **Community left**: %d\n", CommunityLeft())
300		out += ufmt.Sprintf("* **Incentive left**: %d\n", IncentiveLeft())
301	}
302	out += ufmt.Sprintf("* **Proposals**: %d\n", nextProposal)
303	return out
304}
305
306func renderProposal(idText string) string {
307	id, err := strconv.Atoi(idText)
308	if err != nil || id < 0 || id >= nextProposal {
309		return "# WIZ GovDAO\n\nProposal not found.\n"
310	}
311	p := proposalOf(id)
312	toText := ""
313	if p.to.IsValid() {
314		toText = p.to.String()
315	}
316	out := ufmt.Sprintf("# WIZ proposal %d\n\n", id)
317	out += ufmt.Sprintf("* **Kind**: %d\n", p.kind)
318	out += ufmt.Sprintf("* **Proposer**: %s\n", p.proposer.String())
319	out += ufmt.Sprintf("* **To**: %s\n", toText)
320	out += ufmt.Sprintf("* **Token**: %d\n", p.tokenAmt)
321	out += renderMemo(p.memo)
322	out += ufmt.Sprintf("* **Yes**: %d\n", p.yes)
323	out += ufmt.Sprintf("* **No**: %d\n", p.no)
324	out += ufmt.Sprintf("* **Quorum**: %d\n", drop.Quorum())
325	out += ufmt.Sprintf("* **End**: %d\n", p.end)
326	out += ufmt.Sprintf("* **Executed**: %t\n", p.executed)
327	out += ufmt.Sprintf("* **Passed**: %t\n", p.passed)
328	return out
329}
330
331func renderMemo(memo string) string {
332	title, summary, change, ok := splitNote(memo)
333	if !ok {
334		return ufmt.Sprintf("* **Memo**: %s\n", oneLine(memo))
335	}
336	out := ufmt.Sprintf("* **Title**: %s\n", oneLine(title))
337	out += ufmt.Sprintf("* **Summary**: %s\n", oneLine(summary))
338	out += ufmt.Sprintf("* **Change**: %s\n", oneLine(change))
339	return out
340}
341
342func splitNote(memo string) (title, summary, change string, ok bool) {
343	if len(memo) < len(notePrefix) || memo[:len(notePrefix)] != notePrefix {
344		return "", "", "", false
345	}
346	parts := strings.SplitN(memo[len(notePrefix):], noteSplit, 3)
347	if len(parts) != 3 {
348		return "", "", "", false
349	}
350	return parts[0], parts[1], parts[2], true
351}
352
353func oneLine(s string) string {
354	b := []byte(s)
355	for i := 0; i < len(b); i++ {
356		if b[i] < 0x20 || b[i] == 0x7f {
357			b[i] = ' '
358		}
359	}
360	return string(b)
361}
362
363func executable(p *proposal) bool {
364	switch p.kind {
365	case KindSpend:
366		return payable(p, CommunityLeft())
367	case KindIncentive:
368		return payable(p, IncentiveLeft())
369	case KindSignal:
370		return true
371	default:
372		return false
373	}
374}
375
376func payable(p *proposal, left int64) bool {
377	if p.tokenAmt <= 0 || p.tokenAmt > left || !p.to.IsValid() || p.to == treasury || p.to == drop.Escrow() {
378		return false
379	}
380	return p.tokenAmt <= drop.BalanceOf(treasury)
381}
382
383func mustPayProposal(to address, tokenAmt int64, left int64) {
384	if !to.IsValid() || to == treasury || to == drop.Escrow() || tokenAmt <= 0 || tokenAmt > left || tokenAmt > drop.BalanceOf(treasury) {
385		panic("proposal")
386	}
387}
388
389func sleeveLeft(sent, spent int64) int64 {
390	if spent < 0 || sent < spent {
391		panic("sleeve")
392	}
393	return sent - spent
394}
395
396func proposalOf(id int) *proposal {
397	if id < 0 || id >= nextProposal {
398		panic("proposal")
399	}
400	v := proposals.Get(strconv.Itoa(id))
401	if v == nil {
402		panic("proposal")
403	}
404	p, ok := v.(*proposal)
405	if !ok || p == nil {
406		panic("proposal")
407	}
408	return p
409}
410
411func voteKey(id int, voter address) string {
412	return strconv.Itoa(id) + ":" + voter.String()
413}
414
415func mustCurrent(cur realm) {
416	if !cur.IsCurrent() {
417		panic("realm")
418	}
419}
420
421func mustUser(cur realm) {
422	mustCurrent(cur)
423	if !cur.Previous().IsUserCall() {
424		panic("user call only")
425	}
426}
427
428func mustNoPayment() {
429	if len(unsafe.OriginSend()) != 0 {
430		panic("unexpected payment")
431	}
432}