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.

bridge.gno

17.94 Kb · 572 lines
  1// Package memba_bridge_v1 governs the ten Memba application realms published
  2// before memba_gov. It becomes each app's admin through the app's own
  3// two-step handover, so the apps' code does not change, and turns memba_gov
  4// approvals into the app's owner-only calls.
  5//
  6// Every governed entrypoint is a direct call by a seated member. It reads the
  7// app's live pre-state, encodes it with the call's arguments and the app's
  8// tenure (how many times the bridge has accepted it), and consumes exactly
  9// that approval from memba_gov before calling the app: an approval voted on
 10// another state, app, action, argument or tenure never matches. Action ids
 11// are "<app>.<Op>"; the invalidation scope is the app, or an object of it
 12// where actions are independent.
 13//
 14// Accept, ExpirePause and the reads need no vote. EmergencyPause needs a
 15// seated member, no vote, and expires by itself. Approval tells a proposer
 16// exactly what an entrypoint call would consume, from the same code.
 17package memba_bridge_v1
 18
 19import (
 20	"chain"
 21	"strconv"
 22	"time"
 23
 24	"gno.land/p/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/daoauth"
 25	gov "gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/memba_gov"
 26	escrow "gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/escrow_v4"
 27	badges "gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/gnobuilders_badges_v2"
 28	appstore "gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/memba_appstore_v3"
 29	arcade "gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/memba_arcade_leaderboard_v1"
 30	channels "gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/memba_dao_channels_v2"
 31	feed "gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/memba_feed_v1"
 32	feedback "gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/memba_feedback_v2"
 33	market "gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/memba_market_config"
 34	quest "gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/memba_quest_attestation_v1"
 35	reviews "gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/memba_reviews_v2"
 36)
 37
 38const (
 39	appMarket   = "memba_market_config"
 40	appEscrow   = "escrow_v4"
 41	appAppstore = "memba_appstore_v3"
 42	appReviews  = "memba_reviews_v2"
 43	appQuest    = "memba_quest_attestation_v1"
 44	appArcade   = "memba_arcade_leaderboard_v1"
 45	appBadges   = "gnobuilders_badges_v2"
 46	appFeed     = "memba_feed_v1"
 47	appChannels = "memba_dao_channels_v2"
 48	appFeedback = "memba_feedback_v2"
 49)
 50
 51var (
 52	self = chain.PackageAddress("gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/memba_bridge_v1")
 53	apps = []string{appMarket, appEscrow, appAppstore, appReviews, appQuest, appArcade, appBadges, appFeed, appChannels, appFeedback}
 54	// Addresses no governed argument may name: fees sent there are locked
 55	// for good, and a role given to one of them is unusable.
 56	protected   = map[address]bool{}
 57	tenure      = map[string]uint64{}    // app -> times the bridge accepted it
 58	pausedUntil = map[string]time.Time{} // app -> end of the bridge's pause
 59	episodes    = map[string]uint64{}    // app -> pauses the bridge started
 60	lastPause   = map[string]time.Time{} // member id -> last emergency pause
 61)
 62
 63func init() {
 64	for _, path := range []string{"memba_bridge_v1", "memba_gov", "memba_dao", "escrow_v3"} {
 65		protected[chain.PackageAddress("gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/"+path)] = true
 66	}
 67	for _, app := range apps {
 68		protected[chain.PackageAddress("gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/"+app)] = true
 69	}
 70}
 71
 72// ── Authorisation ───────────────────────────────────────────────────────
 73
 74// direct returns the account that called the bridge directly.
 75func direct(cur realm) address {
 76	if !cur.IsCurrent() || !cur.Previous().IsUserCall() {
 77		panic("memba_bridge: call directly from your account")
 78	}
 79	return cur.Previous().Address()
 80}
 81
 82// A plan is the approval one governed call needs: action app.op, args
 83// (completed by the app's tenure, which voids approvals voted while the app
 84// was away), the invalidation scope and the minimum class.
 85type plan struct {
 86	app, op string
 87	args    *daoauth.Args
 88	scope   string
 89	class   int
 90}
 91
 92// newPlan refuses an approval for an app the bridge does not govern now: no
 93// call could consume it.
 94func newPlan(app, op string, args *daoauth.Args, scope string, class int) plan {
 95	if !governs(app) {
 96		panic("memba_bridge: the bridge does not govern " + app)
 97	}
 98	return plan{app, op, args, scope, class}
 99}
100
101func (p plan) voted() string { return p.args.Uint(tenure[p.app]).String() }
102
103// approve consumes proposal pid for p; memba_gov checks the executor, the
104// bridge's direct caller, is a seated member.
105func approve(cur realm, pid uint64, p plan) {
106	executor := direct(cur)
107	gov.Consume(cross(cur), pid, p.app+"."+p.op, p.voted(), p.scope, p.class, executor)
108}
109
110// Approval returns, as JSON, the proposal a governed entrypoint call would
111// consume if made now: target this realm, with the action, args, scope and
112// minimum class given. call is the entrypoint's name and its arguments after
113// pid, daoauth-encoded in order (strings s:, addresses a:, integers i: or
114// u:, booleans b:). It refuses a call its entrypoint would refuse before
115// voting, and holds only while the state it binds is unchanged.
116func Approval(call string) string {
117	c := parseCall(call)
118	var p plan
119	switch name := c.str(); name {
120	case "TransferAdmin":
121		p = transferPlan(c.str(), c.addr())
122	case "CancelTransfer":
123		p = cancelPlan(c.str())
124	case "SetPause":
125		p = pausePlan(c.str(), c.i64())
126	case "Grant", "Revoke":
127		p = rolePlan(c.str(), c.addr(), name == "Grant")
128	case "Curate":
129		p = curatePlan(c.str(), c.str(), c.str())
130	case "SetRegistrationFee":
131		p = registrationFeePlan(c.i64())
132	case "SetTreasury":
133		p = treasuryPlan(c.str(), c.addr())
134	case "SetFee":
135		p = feePlan(c.str(), c.i64())
136	case "ResolveDispute":
137		p, _ = disputePlan(c.str(), int(c.i64()), c.flag())
138	case "ProposeFeeRecipient":
139		p = feeRecipientPlan(c.addr())
140	case "CancelFeeRecipient":
141		p = cancelFeeRecipientPlan()
142	case "HideReview", "HideComment", "Unhide":
143		p = moderationPlan(name, c.u64())
144	case "SetQuestSigner":
145		p = signerPlan(c.str())
146	case "AddMember", "SetRoles":
147		p = memberPlan(c.str(), name, c.addr(), c.str())
148	case "RemoveMember":
149		p = memberPlan(c.str(), name, c.addr(), "")
150	case "CreateChannel":
151		p = channelPlan(c.str(), c.str(), c.str(), c.str())
152	default:
153		panic("memba_bridge: no governed entrypoint " + name)
154	}
155	c.done()
156	return `{"target":"gno.land/r/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/memba_bridge_v1","action":` + quote(p.app+"."+p.op) +
157		`,"args":` + quote(p.voted()) + `,"scope":` + quote(p.scope) + `,"class":` + strconv.Itoa(p.class) + `}`
158}
159
160// call reads an Approval call's fields in order, each with its tag.
161type call struct{ f []daoauth.Field }
162
163func parseCall(s string) *call {
164	f, err := daoauth.Parse(s)
165	if err != nil {
166		panic(err.Error())
167	}
168	return &call{f}
169}
170
171func (c *call) next(tag byte) string {
172	if len(c.f) == 0 || c.f[0].Tag != tag {
173		panic("memba_bridge: call argument " + string(tag) + ": expected")
174	}
175	v := c.f[0].Value
176	c.f = c.f[1:]
177	return v
178}
179
180// daoauth.Parse has checked each value is canonical for its tag.
181func (c *call) str() string   { return c.next('s') }
182func (c *call) addr() address { return address(c.next('a')) }
183func (c *call) i64() int64 {
184	n, _ := strconv.ParseInt(c.next('i'), 10, 64)
185	return n
186}
187
188func (c *call) u64() uint64 {
189	n, _ := strconv.ParseUint(c.next('u'), 10, 64)
190	return n
191}
192
193func (c *call) flag() bool { return c.next('b') == "1" }
194
195func (c *call) done() {
196	if len(c.f) != 0 {
197		panic("memba_bridge: too many call arguments")
198	}
199}
200
201// quote returns s, printable ASCII, as a JSON string with < > & escaped.
202func quote(s string) string {
203	b := make([]byte, 0, 6*len(s)+2)
204	b = append(b, '"')
205	for i := 0; i < len(s); i++ {
206		switch c := s[i]; c {
207		case '"', '\\':
208			b = append(b, '\\', c)
209		case '<', '>', '&':
210			b = append(b, `\u00`...)
211			b = append(b, "0123456789abcdef"[c>>4], "0123456789abcdef"[c&0xf])
212		default:
213			b = append(b, c)
214		}
215	}
216	return string(append(b, '"'))
217}
218
219func checkApp(app string) {
220	for _, a := range apps {
221		if a == app {
222			return
223		}
224	}
225	panic("memba_bridge: unknown app " + app)
226}
227
228// arg refuses a non-canonical or protected address as a recipient of a role,
229// membership, admin or fees.
230func arg(a address) address {
231	if protected[a] {
232		panic("memba_bridge: address must not be a governance or app realm")
233	}
234	return canonical(a)
235}
236
237// canonical refuses an address the chain would never give a caller.
238func canonical(a address) address {
239	if !daoauth.ValidAddress(a) {
240		panic("memba_bridge: address must be lowercase bech32")
241	}
242	return a
243}
244
245// ── Admin handover (every app) ──────────────────────────────────────────
246
247// Accept takes the admin role the 2-of-3 nominated the bridge for. It needs
248// no vote: the nomination is the decision. A paused app is refused: only a
249// pause the bridge started can be ended by its rules.
250func Accept(cur realm, app string) {
251	checkApp(app)
252	if hasPause(app) && isPaused(app) {
253		panic("memba_bridge: unpause " + app + " before handing it over")
254	}
255	switch app {
256	case appMarket:
257		market.AcceptAdmin(cross(cur))
258	case appEscrow:
259		escrow.AcceptOwnership(cross(cur))
260	case appAppstore:
261		appstore.AcceptOwnership(cross(cur))
262	case appReviews:
263		reviews.AcceptOwnership(cross(cur))
264	case appQuest:
265		quest.AcceptOwnership(cross(cur))
266	case appArcade:
267		arcade.AcceptOwnership(cross(cur))
268	case appBadges:
269		badges.AcceptOwnership(cross(cur))
270	case appFeed:
271		feed.AcceptOwnership(cross(cur))
272	case appChannels:
273		channels.AcceptOwnership(cross(cur))
274	case appFeedback:
275		feedback.AcceptOwnership(cross(cur))
276	}
277	tenure[app]++
278	delete(pausedUntil, app) // a record from an earlier tenure never applies
279}
280
281// TransferAdmin stages to as the app's next admin: the 2-of-3, a successor
282// bridge, or anyone the DAO names. The recipient accepts by its own call.
283func TransferAdmin(cur realm, pid uint64, app string, to address) {
284	approve(cur, pid, transferPlan(app, to))
285	switch app {
286	case appMarket:
287		market.TransferAdmin(cross(cur), to)
288	case appEscrow:
289		escrow.TransferOwnership(cross(cur), to)
290	case appAppstore:
291		appstore.TransferOwnership(cross(cur), to)
292	case appReviews:
293		reviews.TransferOwnership(cross(cur), to)
294	case appQuest:
295		quest.TransferOwnership(cross(cur), to)
296	case appArcade:
297		arcade.TransferOwnership(cross(cur), to)
298	case appBadges:
299		badges.TransferOwnership(cross(cur), to)
300	case appFeed:
301		feed.TransferOwnership(cross(cur), to)
302	case appChannels:
303		channels.ProposeOwner(cross(cur), to)
304	case appFeedback:
305		feedback.TransferOwnership(cross(cur), to)
306	}
307}
308
309func transferPlan(app string, to address) plan {
310	checkApp(app)
311	return newPlan(app, "TransferAdmin", daoauth.New().Addr(arg(to)).Str(pendingAdmin(app)), app, gov.Critical)
312}
313
314// CancelTransfer withdraws a staged admin before the recipient accepts.
315func CancelTransfer(cur realm, pid uint64, app string) {
316	approve(cur, pid, cancelPlan(app))
317	switch app {
318	case appMarket:
319		market.CancelAdminTransfer(cross(cur))
320	case appEscrow:
321		escrow.CancelOwnershipTransfer(cross(cur))
322	case appAppstore:
323		appstore.CancelOwnershipTransfer(cross(cur))
324	case appReviews:
325		reviews.CancelOwnershipTransfer(cross(cur))
326	case appQuest:
327		quest.CancelOwnershipTransfer(cross(cur))
328	case appArcade:
329		arcade.CancelOwnershipTransfer(cross(cur))
330	case appBadges:
331		badges.CancelOwnershipTransfer(cross(cur))
332	case appFeed:
333		feed.CancelOwnershipTransfer(cross(cur))
334	case appChannels:
335		channels.CancelPendingOwner(cross(cur))
336	case appFeedback:
337		feedback.CancelOwnershipTransfer(cross(cur))
338	}
339}
340
341func cancelPlan(app string) plan {
342	checkApp(app)
343	pending := pendingAdmin(app)
344	if pending == "" {
345		panic("memba_bridge: nothing staged to cancel for " + app)
346	}
347	return newPlan(app, "CancelTransfer", daoauth.New().Str(pending), app, gov.Financial)
348}
349
350// governs reports whether the bridge is app's admin now.
351func governs(app string) bool {
352	var admin string
353	switch app {
354	case appMarket:
355		admin = string(market.GetAdmin())
356	case appEscrow:
357		admin = escrow.GetAdmin()
358	case appAppstore:
359		admin = string(appstore.GetOwner())
360	case appReviews:
361		admin = reviews.GetModerator()
362	case appQuest:
363		admin = quest.GetOwner()
364	case appArcade:
365		admin = string(arcade.GetOwner())
366	case appBadges:
367		admin = string(badges.GetOwner())
368	case appFeed:
369		admin = string(feed.GetOwner())
370	case appChannels:
371		admin = string(channels.GetOwner())
372	case appFeedback:
373		admin = string(feedback.GetOwner())
374	default:
375		return false
376	}
377	return admin == string(self)
378}
379
380func pendingAdmin(app string) string {
381	switch app {
382	case appMarket:
383		return string(market.GetPendingAdmin())
384	case appEscrow:
385		return escrow.GetPendingAdmin()
386	case appAppstore:
387		return string(appstore.GetPendingOwner())
388	case appReviews:
389		return reviews.GetPendingModerator()
390	case appQuest:
391		return quest.GetPendingOwner()
392	case appArcade:
393		return string(arcade.GetPendingOwner())
394	case appBadges:
395		return string(badges.GetPendingOwner())
396	case appFeed:
397		return string(feed.GetPendingOwner())
398	case appChannels:
399		return string(channels.GetPendingOwner())
400	}
401	return string(feedback.GetPendingOwner())
402}
403
404// ── Pause (escrow, App Store, arcade, badges, feed, channels, feedback) ──
405
406// EmergencyPause pauses app for 7 days, without a vote. Each member may use
407// it once per 30 days, so one key cannot keep an app closed.
408func EmergencyPause(cur realm, app string) {
409	id := gov.PersonOf(direct(cur))
410	if id == "" {
411		panic("memba_bridge: not a seated member")
412	}
413	if !governs(app) {
414		panic("memba_bridge: the bridge does not govern " + app)
415	}
416	// A paused app is refused first, and costs no allowance: an emergency
417	// pause must never shorten a longer pause members voted.
418	if isPaused(app) {
419		panic("memba_bridge: already paused")
420	}
421	now := time.Now()
422	if last, ok := lastPause[id]; ok && now.Before(last.Add(PauseAllowance)) {
423		panic("memba_bridge: one emergency pause per member per 30 days")
424	}
425	setPaused(cur, app, true)
426	pausedUntil[app] = now.Add(EmergencyPauseFor)
427	episodes[app]++
428	lastPause[id] = now
429}
430
431// SetPause ends the bridge's pause of app now (until = 0) or keeps app
432// paused until the unix time until, at most 30 days ahead. The approval names
433// the pause episode, so it never ends a later pause.
434func SetPause(cur realm, pid uint64, app string, until int64) {
435	approve(cur, pid, pausePlan(app, until))
436	if until == 0 {
437		if isPaused(app) {
438			setPaused(cur, app, false)
439		}
440		delete(pausedUntil, app)
441		return
442	}
443	if !isPaused(app) {
444		setPaused(cur, app, true)
445		episodes[app]++
446	}
447	pausedUntil[app] = time.Unix(until, 0)
448}
449
450func pausePlan(app string, until int64) plan {
451	now := time.Now()
452	if end := time.Unix(until, 0); until != 0 && (!end.After(now) || end.After(now.Add(MaxPauseAhead))) {
453		panic("memba_bridge: pause end must be within the next 30 days")
454	}
455	// Governance first: an app the bridge does not govern says so before its pause is read.
456	p := newPlan(app, "SetPause", daoauth.New(), app, gov.Financial)
457	p.args.Int(until).Bool(isPaused(app)).Uint(episodes[app])
458	return p
459}
460
461// ExpirePause lets anyone end the bridge's pause of app once it is over. A
462// SetPause that keeps app paused longer must execute before then. Once the
463// bridge no longer governs app, its record no longer applies: anyone drops
464// it, and the app, now its new admin's, is left as it is.
465func ExpirePause(cur realm, app string) {
466	end, ok := pausedUntil[app]
467	if ok && !governs(app) {
468		delete(pausedUntil, app)
469		return
470	}
471	if !ok || time.Now().Before(end) {
472		panic("memba_bridge: no expired pause for " + app)
473	}
474	delete(pausedUntil, app)
475	if isPaused(app) {
476		setPaused(cur, app, false)
477	}
478}
479
480func hasPause(app string) bool {
481	return app != appMarket && app != appReviews && app != appQuest
482}
483
484func isPaused(app string) bool {
485	switch app {
486	case appEscrow:
487		return escrow.IsPaused()
488	case appAppstore:
489		return appstore.IsPaused()
490	case appArcade:
491		return arcade.IsPaused()
492	case appBadges:
493		return badges.IsPaused()
494	case appFeed:
495		return feed.IsPaused()
496	case appChannels:
497		return channels.IsPaused()
498	case appFeedback:
499		return feedback.IsPaused()
500	}
501	panic("memba_bridge: " + app + " has no pause")
502}
503
504func setPaused(cur realm, app string, on bool) {
505	switch {
506	case app == appEscrow && on:
507		escrow.Pause(cross(cur))
508	case app == appEscrow:
509		escrow.Unpause(cross(cur))
510	case app == appAppstore:
511		appstore.Pause(cross(cur), on)
512	case app == appArcade:
513		arcade.Pause(cross(cur), on)
514	case app == appBadges && on:
515		badges.Pause(cross(cur))
516	case app == appBadges:
517		badges.Unpause(cross(cur))
518	case app == appFeed && on:
519		feed.PauseRealm(cross(cur))
520	case app == appFeed:
521		feed.UnpauseRealm(cross(cur))
522	case app == appChannels && on:
523		channels.PauseRealm(cross(cur))
524	case app == appChannels:
525		channels.UnpauseRealm(cross(cur))
526	case app == appFeedback && on:
527		feedback.PauseRealm(cross(cur))
528	case app == appFeedback:
529		feedback.UnpauseRealm(cross(cur))
530	}
531}
532
533func unixOf(t time.Time) int64 {
534	if t.IsZero() {
535		return 0
536	}
537	return t.Unix()
538}
539
540// ── Reads ───────────────────────────────────────────────────────────────
541
542// PausedUntil returns the unix end of the bridge's pause of app, or 0 (also
543// once the bridge no longer governs app).
544func PausedUntil(app string) int64 {
545	if !governs(app) {
546		return 0
547	}
548	return unixOf(pausedUntil[app])
549}
550
551// PauseEpisode returns how many pauses of app the bridge has started; a
552// SetPause approval binds it.
553func PauseEpisode(app string) uint64 { return episodes[app] }
554
555// Tenure returns how many times the bridge has accepted app; every approval
556// for app binds it as its last argument.
557func Tenure(app string) uint64 { return tenure[app] }
558
559func Render(_ string) string {
560	out := "# Memba bridge v1\n\nGoverns the ten Memba apps for memba_gov. Bridge pauses:\n\n"
561	n := 0
562	for _, app := range apps {
563		if end, ok := pausedUntil[app]; ok && governs(app) {
564			out += "- " + app + " until unix " + strconv.FormatInt(end.Unix(), 10) + "\n"
565			n++
566		}
567	}
568	if n == 0 {
569		out += "None.\n"
570	}
571	return out
572}