1// Package upkeep pays people to run the ecosystem's permissionless2// valves. The portfolio's realms deliberately expose maintenance3// entrypoints that anyone may call — subscriptions.Expire frees a lapsed4// subscription slot, timelock_guardian.Execute fires a matured timelocked5// action — because no slot's liveness may depend on an interested party6// showing up. This realm adds the missing economics: funders finance a7// reward pot, and whoever triggers a valve THROUGH this realm is credited8// a bounded reward, claimable by pull.9//10// THE REALM-TO-REALM BOUNDARY, PRECISELY:11//12// which calls which : upkeep -> subscriptions.Expire(cross, id)13// upkeep -> timelock_guardian.Execute(cross, id)14// why it is necessary : the reward must be conditioned on the valve15// actually firing. Only making the call from16// inside this realm ties "the valve fired" and17// "the reward is credited" into one atomic18// transaction; observing from outside cannot.19// caller identity : downstream sees cur.Previous() = THIS realm,20// not the poking EOA. Both valves are21// permissionless BY DESIGN and use the caller22// identity for nothing, so the intermediary23// changes no authorization outcome. This realm24// is not a deputy for any downstream authority25// — it holds none to confuse.26// authorization boundary: upstream, anyone may poke (the reward is the27// only thing at stake and the pot is the only28// source). Downstream, each valve enforces its29// own STATE conditions (grace elapsed, delay30// matured) exactly as it would for a direct31// caller.32// ordering : downstream call FIRST, reward accounting33// AFTER. A downstream abort therefore reverts34// the whole transaction before any pot or35// ledger mutation exists.36// failure behavior : any downstream panic (not expirable, too37// early, already executed, unknown id...)38// aborts this transaction. No partial state,39// no reward, by VM atomicity — not by cleanup40// code.41// atomicity assumptions : a Gno transaction is all-or-nothing across42// realm boundaries; there is no catch/recover43// anywhere on this path (and none may be added44// — recovery would break exactly this45// guarantee).46// value movement : none crosses the boundary. Both valves move47// no coins; the poke transaction must carry no48// coins (subscriptions' assertNoSend reads the49// ORIGIN envelope unconditionally — measured,50// not assumed). Rewards move only inside this51// realm's ledger, funded by explicit Fund52// transactions.53// downstream rejection : reward denied automatically — the abort is54// the denial.55// downstream trust : neither valve trusts nor validates the56// caller; both validate state. This realm57// symmetrically does not trust the downstream58// REPLY beyond "it did not abort".59// replay : enforced downstream. A second Expire on the60// same subscription aborts ("subscription is61// expired"); a second Execute aborts ("action62// already executed"). A poker cannot be paid63// twice for one valve event.64// adversarial callers : an EOA or realm poking with bogus ids,65// premature targets, or replays hits a66// downstream abort and pays its own gas. The67// remaining economic edge — manufacturing68// expirable state to farm rewards — differs69// per task. sub_expire: every farmed70// subscription permanently locks a sub-record71// storage deposit of roughly 30,000ugnot at72// the observed 100ugnot/byte rate (Expire73// flips status; it does not free the record),74// which exceeds MaxReward before price and75// gas — loss-making at any legal setting.76// timelock_execute: a farmed Action record is77// small and its deposit can sit below the78// cap, so farming resistance there rests on79// the admin keeping the setting below the80// measured cycle cost. Funders trust the cap81// AND the admin's reward policy, not the cap82// alone. The pot is a donation either way —83// farming can drain it, never third-party84// balances.85//86// Rewards default to 0 per task; the admin sets them within the87// compile-time MaxReward. The pot only moves down via successful pokes88// and only up via Fund. Conservation: Held == pot + UsersTotal89// (+ out-of-band surplus, recoverable above that reserve by SweepDenom).90// The ledger's fee cap is 0 — no fee exists anywhere in this realm.91package upkeep9293import (94 "chain"95 "chain/runtime/unsafe"96 "strconv"9798 "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio"99 "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/feeledger"100101 subs "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/subscriptions"102 guardian "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/timelock_guardian"103)104105// Denom is the only asset this realm accepts.106const Denom = "ugnot"107108// Task identifiers — the two supported valves.109const (110 TaskSubExpire = "sub_expire"111 TaskTimelockExecute = "timelock_execute"112)113114// MaxReward is the compile-time ceiling on the per-poke reward:115// 20,000ugnot (0.02 GNOT). It bounds what any single poke can extract116// from the pot. For sub_expire it also defeats farming outright: one117// manufactured expirable subscription permanently locks a sub-record118// deposit of ~30,000ugnot at the observed 100ugnot/byte rate — already119// above the cap before price and gas. For timelock_execute the farmed120// Action record is smaller and its deposit can sit below the cap, so121// there the admin's setting, kept below the measured farm-cycle cost,122// is what deters farming. Funders trust the cap and the admin's123// policy together; the pot they fund is an explicit donation either124// way.125const MaxReward = int64(20000)126127var (128 admin address // may set rewards, stage a successor129 pendingAdmin address // staged by TransferAdmin, completes via AcceptAdmin130 sweeper address // may sweep out-of-band surplus131132 self address // this realm's address, captured at deploy133 pot int64 // funded, not-yet-awarded ugnot134135 rewards = map[string]int64{136 TaskSubExpire: 0,137 TaskTimelockExecute: 0,138 }139 ledger = feeledger.MustNew(0)140141 // lifetime counters, rendered for operators142 pokes int64143 funded int64144)145146func init() {147 admin = unsafe.OriginCaller()148 sweeper = admin149 self = unsafe.CurrentRealm().Address()150}151152// --- funding ---153154// Fund adds the attached coins to the reward pot. Anyone may fund;155// funding is a donation to ecosystem maintenance and is not refundable.156func Fund(cur realm) {157 funder, amount := coinio.Receive(0, cur, Denom)158 newPot, ok := checkedAdd(pot, amount)159 if !ok {160 panic("pot would overflow")161 }162 pot = newPot163 funded += amount164 chain.Emit("Funded", "from", funder.String(), "amount", itoa(amount),165 "pot", itoa(pot))166}167168// --- the pokes: the realm-to-realm calls ---169170// PokeExpire triggers subscriptions.Expire(subID) through this realm and171// credits the caller the sub_expire reward. The downstream realm decides172// whether the subscription is expirable; its abort is the authorization.173// The transaction must attach no coins (the downstream realm checks the174// origin envelope).175func PokeExpire(cur realm, subID int64) {176 assertNoSend()177 caller := cur.Previous().Address()178 reward := requireReward(TaskSubExpire)179180 // Downstream first: an abort here reverts everything below.181 subs.Expire(cross(cur), subID)182183 award(caller, reward, TaskSubExpire, itoa(subID))184}185186// PokeExecute triggers timelock_guardian.Execute(actionID) through this187// realm and credits the caller the timelock_execute reward. The guardian188// decides whether the action is executable; its abort is the189// authorization.190func PokeExecute(cur realm, actionID string) {191 assertNoSend()192 caller := cur.Previous().Address()193 reward := requireReward(TaskTimelockExecute)194195 guardian.Execute(cross(cur), actionID)196197 award(caller, reward, TaskTimelockExecute, actionID)198}199200// award moves reward from the pot to the caller's claimable balance.201// Callers have already established that the pot covers it.202func award(caller address, reward int64, task, target string) {203 pot -= reward204 ledger.MustDeposit(caller.String(), reward, 0)205 pokes++206 chain.Emit("Poked",207 "task", task,208 "target", target,209 "caller", caller.String(),210 "reward", itoa(reward),211 "pot", itoa(pot),212 )213}214215// requireReward returns the configured reward for a task, refusing when216// the task is unknown, unrewarded, or the pot cannot cover it. Refusal217// on an empty pot is deliberate: both valves remain directly callable on218// their own realms, so an unrewarded detour through this one is only a219// gas trap for the caller.220func requireReward(task string) int64 {221 r, ok := rewards[task]222 if !ok {223 panic("unknown task: " + task)224 }225 if r <= 0 {226 panic("no reward configured for " + task)227 }228 if pot < r {229 panic("reward pot cannot cover " + task + ": pot " + itoa(pot) +230 ", reward " + itoa(r))231 }232 return r233}234235// --- claims ---236237// Claim sends amount ugnot of the caller's earned balance back to the238// caller.239func Claim(cur realm, amount int64) {240 assertNoSend()241 caller := cur.Previous().Address()242 if err := ledger.Withdraw(caller.String(), amount); err != nil {243 panic(err)244 }245 coinio.Payout(0, cur, caller, Denom, amount)246 chain.Emit("Claim", "to", caller.String(), "amount", itoa(amount))247}248249// ClaimAll sends the caller's entire earned balance back to the caller.250func ClaimAll(cur realm) {251 assertNoSend()252 caller := cur.Previous().Address()253 amount, err := ledger.WithdrawAll(caller.String())254 if err != nil {255 panic(err)256 }257 if amount == 0 {258 panic("nothing to claim")259 }260 coinio.Payout(0, cur, caller, Denom, amount)261 chain.Emit("Claim", "to", caller.String(), "amount", itoa(amount))262}263264// --- administration ---265266// SetReward configures the per-poke reward for a task, bounded by267// MaxReward. Admin only. Zero disables the task.268func SetReward(cur realm, task string, amount int64) {269 assertNoSend()270 assertAdmin(cur.Previous().Address())271 if _, ok := rewards[task]; !ok {272 panic("unknown task: " + task)273 }274 if amount < 0 || amount > MaxReward {275 panic("reward must be in [0, " + itoa(MaxReward) + "]")276 }277 old := rewards[task]278 rewards[task] = amount279 chain.Emit("RewardChanged", "task", task, "old", itoa(old), "new", itoa(amount))280}281282// SweepDenom recovers out-of-band coins to the sweeper. For the pot283// denom the reserve is pot + earned balances — both structurally284// unreachable. Sweeper only.285func SweepDenom(cur realm, denom string) {286 assertNoSend()287 if cur.Previous().Address() != sweeper {288 panic("sweeper only")289 }290 reserve := int64(0)291 if denom == Denom {292 reserve = pot + ledger.Liabilities()293 }294 swept := coinio.Sweep(0, cur, sweeper, denom, reserve)295 chain.Emit("SurplusSwept", "to", sweeper.String(), "coins", itoa(swept)+denom)296}297298// TransferAdmin stages a two-step admin handover.299func TransferAdmin(cur realm, successor address) {300 assertNoSend()301 assertAdmin(cur.Previous().Address())302 var zero address303 if successor == zero {304 panic("successor must not be the zero address")305 }306 pendingAdmin = successor307 chain.Emit("AdminTransferStaged", "from", admin.String(), "to", successor.String())308}309310// AcceptAdmin completes the handover; only the staged successor may.311// The sweeper role moves with the admin.312func AcceptAdmin(cur realm) {313 assertNoSend()314 caller := cur.Previous().Address()315 if caller != pendingAdmin {316 panic("only the staged successor may accept")317 }318 old := admin319 admin = caller320 sweeper = caller321 var zero address322 pendingAdmin = zero323 chain.Emit("AdminTransferred", "from", old.String(), "to", admin.String())324}325326// --- views ---327328func Admin() address { return admin }329func PendingAdmin() address { return pendingAdmin }330func Pot() int64 { return pot }331func RewardFor(task string) int64 {332 r, ok := rewards[task]333 if !ok {334 panic("unknown task: " + task)335 }336 return r337}338func BalanceOf(a address) int64 { return ledger.BalanceOf(a.String()) }339func UsersTotal() int64 { return ledger.UsersTotal() }340func Pokes() int64 { return pokes }341func Funded() int64 { return funded }342func Address() address { return self }343func Held() int64 { return coinio.HeldAt(self, Denom) }344345// --- render ---346347func Render(path string) string {348 if path != "" {349 return "unknown page; try the realm root"350 }351 out := "# upkeep\n\n"352 out += "Rewards for running the ecosystem's permissionless valves. " +353 "Fund the pot; poke a valve through this realm; claim what you " +354 "earn. A poke succeeds only when the downstream realm accepts " +355 "the valve call in the same transaction.\n\n"356 out += "- pot: " + itoa(pot) + Denom + "\n"357 out += "- earned, unclaimed: " + itoa(ledger.UsersTotal()) + Denom + "\n"358 out += "- lifetime pokes: " + itoa(pokes) + "\n"359 out += "- lifetime funding: " + itoa(funded) + Denom + "\n\n"360 out += "## rewards per poke\n\n"361 out += "- `" + TaskSubExpire + "` (subscriptions.Expire): " +362 itoa(rewards[TaskSubExpire]) + Denom + "\n"363 out += "- `" + TaskTimelockExecute + "` (timelock_guardian.Execute): " +364 itoa(rewards[TaskTimelockExecute]) + Denom + "\n"365 out += "\n(cap per poke: " + itoa(MaxReward) + Denom + ", compile-time)\n"366 return out367}368369// --- internals ---370371func assertNoSend() {372 if len(unsafe.OriginSend()) != 0 {373 panic("this function does not accept coins")374 }375}376377func assertAdmin(caller address) {378 if caller != admin {379 panic("admin only")380 }381}382383func checkedAdd(a, b int64) (int64, bool) {384 if b > 0 && a > int64(^uint64(0)>>1)-b {385 return 0, false386 }387 return a + b, true388}389390func itoa(n int64) string { return strconv.FormatInt(n, 10) }391AcceptAdmin(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})
Address() string
Admin() string
BalanceOf(a string) int64
Claim(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}, amount int64)
ClaimAll(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})
Fund(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})
Funded() int64
Held() int64
PendingAdmin() string
PokeExecute(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}, actionID string)
PokeExpire(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}, subID int64)
Pokes() int64
Pot() int64
Render(path string) string
RewardFor(task string) int64
SetReward(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}, task string, amount int64)
SweepDenom(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}, denom string)
TransferAdmin(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}, successor string)
UsersTotal() int64
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.