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

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}