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.

keeper.gno

5.80 Kb · 147 lines
  1package nslogic
  2
  3import (
  4	"chain"
  5	"strconv"
  6	"time"
  7
  8	"gno.land/r/nym-mnsrehearsal001/nsdata/v1"
  9)
 10
 11/*
 12THE KEEPER: a hot key that moves the GNOT/USD rate and nothing else
 13(Ely, 2026-10-16).
 14
 15WHY A ROLE. The rate is MANUAL (pricing.gno, SetUsdRate): it sits where it
 16was last written, and a stale rate during a sharp move sells names at the
 17wrong price. Keeping it current is a job for a bot, and a bot lives on a
 18server. The admin key must not, so the bot gets a key of its own that can
 19do exactly one thing.
 20
 21WHAT IT CAN DO. Call SetUsdRate inside the bounds every instant change
 22obeys (timelock.gno): a higher rate — GNOT fell — up to four times at once,
 23every ten minutes, so a crash is followed at once; a lower rate — the
 24direction that sells names cheap — at most 25%, once an hour. The clocks
 25are the SAME ones the admin's instant moves use, so the two keys together
 26still cheapen names one step an hour. Never through the timelock: a
 27matured proposal belongs to the admin, and the keeper's call refuses an
 28out-of-bound value even while one is waiting. Every other function refuses
 29it, because every other function asks for the admin; the admin keeps
 30SetUsdRate as before.
 31
 32WHAT ITS THEFT COSTS. Names a quarter cheaper each hour, in public — each
 33move emits KeeperAction — until the admin, recovery or the guardian calls
 34ClearKeeper, which is instant; or names made too dear to buy, an outage.
 35Bounded in size, in rate and in time, and it never touches a name or a
 36coin directly.
 37
 38NAMED THROUGH THE TIMELOCK, CLEARED AT ONCE. SetKeeper waits out the vault's
 39action delay ("SetKeeper <addr>" through nsdata.ProposeAction), like naming
 40a granter: otherwise a stolen admin key installs a second key that survives
 41its own rotation. Clearing is the safe direction and is open to all three
 42governance roles. An admin rotation does not clear the keeper by itself;
 43the incident runbook does (MAINNET.md §1b).
 44
 45NEVER THE ADMIN, NEVER RECOVERY. A keeper on the admin's address adds
 46nothing, and one on recovery's puts the cold key's address on a server.
 47Both are refused when it is named, and recovery is refused again at call
 48time, in case a later rotation moved recovery onto the keeper's address.
 49
 50Stored in the vault's globals like the granter, so a logic swap keeps it,
 51and listed in managedGlobal, so SetGlobal cannot write round the timelock.
 52*/
 53
 54const keyKeeper = "keeper" // global: the address that may move the rate
 55
 56// Keeper is the address allowed to move the rate, or "" for none.
 57func Keeper() address { return address(nsdata.GetGlobal(keyKeeper)) }
 58
 59// SetKeeper names the keeper. Admin only, and TIMELOCKED: propose exactly
 60// "SetKeeper <addr>" with nsdata.ProposeAction, then call once it matures.
 61// ClearKeeper removes the role.
 62func SetKeeper(cur realm, a address) {
 63	assertNoSend(cur)
 64	assertIsAdmin(cur)
 65	emitAdmin(cur, "SetKeeper", "addr", a.String())
 66	if !a.IsValid() {
 67		panic("nslogic: not an address; ClearKeeper removes the keeper")
 68	}
 69	if a == nsdata.GetAdmin() || a == nsdata.GetRecovery() {
 70		panic("nslogic: the keeper is a key of its own, never the admin or the recovery key")
 71	}
 72	requireAction(cur, actionID("SetKeeper", a.String()))
 73	setGlobal(cur, keyKeeper, a.String())
 74}
 75
 76// ClearKeeper removes the keeper at once. Admin, recovery or guardian —
 77// the guardian bot's answer to a KeeperAction it did not expect. Clearing
 78// when there is no keeper does nothing and still succeeds, so a bot may
 79// call it without reading first.
 80func ClearKeeper(cur realm) {
 81	assertNoSend(cur)
 82	caller := cur.Previous().Address()
 83	if !isGovernor(caller) {
 84		panic("nslogic: only the admin, recovery or the guardian may clear the keeper")
 85	}
 86	chain.Emit("KeeperCleared", "keeper", Keeper().String(), "by", caller.String())
 87	setGlobal(cur, keyKeeper, "")
 88}
 89
 90// isGovernor is the vault's three roles, read fresh. An unset role matches
 91// nobody.
 92func isGovernor(a address) bool {
 93	if a == "" {
 94		return false
 95	}
 96	return a == nsdata.GetAdmin() || a == nsdata.GetRecovery() || a == nsdata.GetGuardian()
 97}
 98
 99// assertIsKeeper lets the keeper through and nobody else, with the refusal
100// a stranger has always had from an admin call.
101func assertIsKeeper(caller address) {
102	k := Keeper()
103	if k == "" || caller != k {
104		panic("nslogic: restricted to project admin")
105	}
106	if k == nsdata.GetRecovery() {
107		panic("nslogic: the keeper's address is now the recovery key's; name a new keeper")
108	}
109}
110
111/*
112keeperSetRate is SetUsdRate for the keeper: the same range check, the
113bound and the shared hourly slot, and no door into the timelock. Every
114move emits KeeperAction, with the value it replaced.
115*/
116func keeperSetRate(cur realm, ugnotPerUsd int64) {
117	caller := cur.Previous().Address()
118	assertIsKeeper(caller)
119	old := RateUgnotPerUsd()
120	chain.Emit("KeeperAction", "fn", "SetUsdRate", "by", caller.String(),
121		"ugnotPerUsd", fmtInt(ugnotPerUsd), "was", fmtInt(old))
122	if ugnotPerUsd <= 0 || ugnotPerUsd > maxRate {
123		panic("nslogic: a rate is 1.." + strconv.FormatInt(maxRate, 10) + " ugnot per USD")
124	}
125	keeperStep(keyRate, old, ugnotPerUsd)
126	setGlobal(cur, keyRate, strconv.FormatInt(ugnotPerUsd, 10))
127}
128
129// keeperStep is authorizeTweak with the timelock door bricked up: an
130// instant step inside its direction's bound, after that direction's rest,
131// or a refusal. It writes the same clocks, so the admin's next instant
132// move in that direction waits the same time.
133func keeperStep(setting string, old, next int64) {
134	if next == old {
135		return
136	}
137	dearer := next > old // the rate: higher means names cost more GNOT
138	if !withinStep(old, next, dearer) {
139		panic("nslogic: for the keeper, " + stepRefusal(setting, old, next, dearer) +
140			"; a larger move is the admin's, through the vault timelock")
141	}
142	now := time.Now().Unix()
143	if !clockRested(setting, dearer, now) {
144		panic("nslogic: " + restRefusal(setting, dearer) + "; the keeper waits for the next step")
145	}
146	lastTweak.Set(clockKey(setting, dearer), now)
147}