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.

rules.gno

5.72 Kb · 178 lines
  1package governor
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	ufmt "gno.land/p/nt/ufmt/v0"
  8)
  9
 10// rulesKind changes what it takes to pass a kind.
 11//
 12//	govern:rules   "<kind> quorum=2000 threshold=6600 voting=120960 delay=34560 grace=241920 propose=100"
 13//
 14// All six terms. ProposeBps was once missing, and it is where a bad value is
 15// worst: above a hundred percent nobody can open a question under that kind at
 16// all — adopted, unusable, and unfixable by the one kind that changes terms.
 17//
 18// Fields left out keep their value, so lengthening one delay is a short line.
 19//
 20// Space-separated key=value because a governance payload has to be readable
 21// where it is read — a URL, a wallet's confirmation screen, a forum post. JSON
 22// puts a parser between a voter and their intent.
 23type rulesKind struct{}
 24
 25func (k rulesKind) Name() string { return reserved + "rules" }
 26
 27// Describe renders the requested change and NOTHING about the world.
 28//
 29// Deliberately not "from 20% to 30%": that would read live state, so two
 30// voters at different heights would see different questions at the same URL,
 31// and a proposal that lands in between would silently rewrite what everyone
 32// else is voting on.
 33func (k rulesKind) describe(g *Governor, payload string) string {
 34	name, set, err := parseRules(payload)
 35	if err != nil {
 36		return "malformed rules change: " + err.Error()
 37	}
 38	out := "change the rules for `" + name + "`:"
 39	for _, f := range set {
 40		out += ufmt.Sprintf("\n- %s -> %s", f.key, f.human())
 41	}
 42	return out
 43}
 44
 45func (k rulesKind) check(g *Governor, payload string) error {
 46	name, set, err := parseRules(payload)
 47	if err != nil {
 48		return err
 49	}
 50	e := g.entryOf(name)
 51	if e == nil {
 52		return errNoSuchKind
 53	}
 54	if !e.live {
 55		// Retuning something not yet adopted would be the same swap the
 56		// immutable-name rule closes, moved from the code to the terms: change
 57		// a kind's quorum while its adoption vote is open and holders approve
 58		// one set of terms and get another. What is not adopted is not the
 59		// governor's to tune.
 60		return errNotLive
 61	}
 62	// No check for an empty change set. parseRules refuses a payload with
 63	// fewer than two fields, and every field after the first either parses or
 64	// returns an error, so a nil error here guarantees at least one change. A
 65	// guard would be unreachable, and unreachable defence reads as a case
 66	// somebody thought could happen.
 67	next := e.rules
 68	applyRules(&next, set)
 69	// The same sanity the offering path enforces, and literally the same code:
 70	// one function, two callers. A second copy is how the two paths come to
 71	// disagree about what a proposer must hold.
 72	return saneRules(next)
 73}
 74
 75func (k rulesKind) run(g *Governor, dispatch Dispatch, payload string) error {
 76	name, set, err := parseRules(payload)
 77	if err != nil {
 78		return err
 79	}
 80	e := g.entryOf(name)
 81	if e == nil {
 82		return errNoSuchKind
 83	}
 84	next := e.rules
 85	applyRules(&next, set)
 86	// Only the entry changes. Proposals already open kept a COPY of the rules
 87	// they were opened under, so this cannot move the bar under a vote that is
 88	// already being cast.
 89	e.rules = next
 90	return nil
 91}
 92
 93type field struct {
 94	key string
 95	n   int64
 96}
 97
 98func (f field) human() string {
 99	switch f.key {
100	case "quorum", "threshold", "propose":
101		// A percentage as well as the raw figure: basis points are exact and
102		// nobody reads them. Padded by hand because ufmt has no zero-padding
103		// and its width digits apply to %s alone, so %02d prints 1 rather than
104		// 01 and one basis point renders as 0.1% — ten times the truth.
105		return ufmt.Sprintf("%d bps (%s)", f.n, pct(f.n))
106	default:
107		// The same rendering the adoption page uses. It printed a bare "%d
108		// blocks" while adoptKind printed a duration, so the same field read
109		// two different ways depending on which page you arrived at.
110		return span(f.n)
111	}
112}
113
114func parseRules(payload string) (string, []field, error) {
115	parts := strings.Fields(payload)
116	if len(parts) < 2 {
117		return "", nil, govErr("expected a kind name and at least one key=value")
118	}
119	name := parts[0]
120	var set []field
121	for _, p := range parts[1:] {
122		eq := strings.Index(p, "=")
123		if eq <= 0 || eq == len(p)-1 {
124			return "", nil, govErr("expected key=value, got " + p)
125		}
126		key, raw := p[:eq], p[eq+1:]
127		switch key {
128		case "quorum", "threshold", "voting", "delay", "grace", "propose":
129		default:
130			return "", nil, govErr("unknown field " + key)
131		}
132		n, err := strconv.ParseInt(raw, 10, 64)
133		if err != nil || n < 0 {
134			return "", nil, govErr("expected a non-negative number for " + key)
135		}
136		set = append(set, field{key: key, n: n})
137	}
138	return name, set, nil
139}
140
141// applyRules applies an already-validated change set to r. It cannot fail:
142// parseRules has validated every key and value, so a default/error path here would
143// be unreachable (unreachable defence reads as a case somebody thought could happen).
144func applyRules(r *Rules, set []field) {
145	for _, f := range set {
146		switch f.key {
147		case "quorum":
148			r.QuorumBps = f.n
149		case "threshold":
150			r.ThresholdBps = f.n
151		case "voting":
152			r.VotingBlocks = f.n
153		case "delay":
154			r.DelayBlocks = f.n
155		case "grace":
156			r.GraceBlocks = f.n
157		case "propose":
158			r.ProposeBps = f.n
159		}
160	}
161}
162
163// pct renders basis points as a percentage.
164//
165// Padded by hand because ufmt has no zero-padding and its width digits apply
166// to %s alone, so %02d prints 1 rather than 01 and one basis point reads as
167// 0.1% — ten times the truth, on the line a voter actually reads.
168func pct(n int64) string {
169	// n/(bps/100) is the percentage; bps/100 is 100, so the split is n/100 and
170	// n%100. Derived from the constant rather than written as 100, so a change
171	// to the scale cannot leave this rendering the old one.
172	per := bps / 100
173	frac := strconv.FormatInt(n%per, 10)
174	for int64(len(frac)) < 2 {
175		frac = "0" + frac
176	}
177	return ufmt.Sprintf("%d.%s%%", n/per, frac)
178}