Transaction

851050FCFD54C7…32E95C859DEC

Block 652,693 · index 0 · indexed

Summary

Hash
851050FCFD54C7E37925F9510FCD2672623E805FDDB69490211A32E95C859DEC
Block
652,693
Size
35381 bytes
Gas used
50,420,653 / 96,000,000
Fee
96000ugnot
Status
success

Messages

#1AddPackagegno.land/r/samcrew/memba_bridge_v111 arguments
Attached funds
14000000ugnot

Arguments · 11

  1. #1memba_bridge_v1
  2. #2apps.gno
  3. #3package memba_bridge_v1 import ( "chain" "chain/banker" "crypto/sha256" "encoding/hex" "strconv" "strings" "gno.land/p/samcrew/daoauth" gov "gno.land/r/samcrew/memba_gov" escrow "gno.land/r/samcrew/escrow_v4" badges "gno.land/r/samcrew/gnobuilders_badges_v2" appstore "gno.land/r/samcrew/memba_appstore_v3" arcade "gno.land/r/samcrew/memba_arcade_leaderboard_v1" channels "gno.land/r/samcrew/memba_dao_channels_v2" feed "gno.land/r/samcrew/memba_feed_v1" feedback "gno.land/r/samcrew/memba_feedback_v2" market "gno.land/r/samcrew/memba_market_config" quest "gno.land/r/samcrew/memba_quest_attestation_v1" reviews "gno.land/r/samcrew/memba_reviews_v2" ) // ── Roles: App Store curators, arcade attesters, badge admins, feed // moderators. Operational work (curating, attesting, minting, moderating) // is done by the holders the DAO appoints here. func Grant(cur realm, pid uint64, app string, who address) { role(cur, pid, app, who, true) } func Revoke(cur realm, pid uint64, app string, who address) { role(cur, pid, app, who, false) } func role(cur realm, pid uint64, app string, who address, grant bool) { approve(cur, pid, rolePlan(app, who, grant)) switch { case app == appAppstore && grant: appstore.AddCurator(cross(cur), who) case app == appAppstore: appstore.RemoveCurator(cross(cur), who) case app == appArcade && grant: arcade.AddAttester(cross(cur), who) case app == appArcade: arcade.RemoveAttester(cross(cur), who) case app == appBadges && grant: badges.AddAdmin(cross(cur), who) case app == appBadges: badges.RemoveAdmin(cross(cur), who) case app == appFeed && grant: feed.AddModerator(cross(cur), who) default: feed.RemoveModerator(cross(cur), who) } } func rolePlan(app string, who address, grant bool) plan { if grant { arg(who) } else if canonical(who) == self { // Revoking its own curator grant would end DAO curation until a hand-over round trip. panic("memba_bridge: the bridge keeps its own role") } // A stale grant held by another protected realm can be revoked. if hasRole(app, who) == grant { panic("memba_bridge: role already in that state") } op := "Revoke" if grant { op = "Grant" } args := daoauth.New().Addr(who) if app == appBadges { args.Int(int64(badges.GetAdminCount())) } return newPlan(app, op, args, app, gov.Critical) } func hasRole(app string, who address) bool { switch app { case appAppstore: return appstore.IsCurator(string(who)) case appArcade: return arcade.IsAttester(string(who)) case appBadges: return badges.IsAdmin(who) case appFeed: return feed.IsModerator(who) } panic("memba_bridge: " + app + " has no role to grant") } // ── App Store ─────────────────────────────────────────────────────────── // Curate runs a curator action on one listing: approve, reject (with a // reason), delist, restore or clearflags. Every approval binds the listing's // status; approve and restore, which publish, also bind its content, and // clearflags binds the flags it clears. Editing or flagging a listing thus // cannot void a reject or delist vote, and cannot slip new content past one. func Curate(cur realm, pid uint64, op, pkgPath, reason string) { approve(cur, pid, curatePlan(op, pkgPath, reason)) switch op { case "approve": appstore.ApproveApp(cross(cur), pkgPath) case "reject": appstore.RejectApp(cross(cur), pkgPath, reason) case "delist": appstore.DelistApp(cross(cur), pkgPath) case "restore": appstore.RestoreApp(cross(cur), pkgPath) default: appstore.ClearFlags(cross(cur), pkgPath) } } func curatePlan(op, pkgPath, reason string) plan { switch { case op != "approve" && op != "reject" && op != "delist" && op != "restore" && op != "clearflags": panic("memba_bridge: unknown curation " + op) case (op == "reject") != (reason != ""): panic("memba_bridge: a reason goes with reject only") } l := appstore.GetGovernanceListing(pkgPath, op == "clearflags") if !l.Exists { panic("memba_bridge: no listing " + pkgPath) } args := daoauth.New().Str(op).Str(pkgPath).Str(reason).Str(l.Status) if op == "approve" || op == "restore" { args.Str(l.ContentHash) } if op == "clearflags" { keys := sha256.Sum256([]byte(l.ClearKeys)) args.Int(int64(l.Flags)).Str(hex.EncodeToString(keys[:])) } return newPlan(appAppstore, "Curate", args, appAppstore+"/l/"+pkgPath, gov.Routine) } func SetRegistrationFee(cur realm, pid uint64, fee int64) { approve(cur, pid, registrationFeePlan(fee)) appstore.SetRegistrationFee(cross(cur), fee) } func registrationFeePlan(fee int64) plan { if fee < 0 || fee > appstore.MaxRegistrationFee { panic("memba_bridge: the registration fee is 0 to " + strconv.FormatInt(appstore.MaxRegistrationFee, 10) + " ugnot") } return newPlan(appAppstore, "SetRegistrationFee", daoauth.New().Int(fee).Int(appstore.GetRegistrationFee()), appAppstore, gov.Financial) } // SetTreasury points the fees of market_config (which escrow also pays // into) or of the App Store at addr. func SetTreasury(cur realm, pid uint64, app string, addr address) { approve(cur, pid, treasuryPlan(app, addr)) if app == appMarket { market.SetTreasury(cross(cur), addr) } else { appstore.SetTreasury(cross(cur), addr) } } func treasuryPlan(app string, addr address) plan { var current address switch app { case appMarket: current = market.GetTreasury() case appAppstore: current = appstore.GetTreasury() default: panic("memba_bridge: " + app + " has no treasury") } return newPlan(app, "SetTreasury", daoauth.New().Addr(arg(addr)).Str(string(current)), app, gov.Critical) } // ── Market config ─────────────────────────────────────────────────────── func SetFee(cur realm, pid uint64, lane string, bps int64) { approve(cur, pid, feePlan(lane, bps)) market.SetFeeBPS(cross(cur), lane, bps) } func feePlan(lane string, bps int64) plan { if lane == "" || bps < 0 || bps > market.MaxFeeBPS { panic("memba_bridge: a fee names its lane and is 0 to " + strconv.FormatInt(market.MaxFeeBPS, 10) + " bps") } return newPlan(appMarket, "SetFee", daoauth.New().Str(lane).Int(bps).Int(int64(market.GetFeeBPS(lane))), appMarket, gov.Financial) } // ── Escrow ────────────────────────────────────────────────────────────── // ResolveDispute settles one disputed milestone: refund pays the client // the whole amount, otherwise the freelancer is paid less the fee. Either // way escrow pays out at most the milestone's amount, which is checked. A // milestone is disputed at most once (a resolution is final); binding the // height of that dispute means no approval can be voted before it exists. func ResolveDispute(cur realm, pid uint64, contractID string, milestone int, refund bool) { p, amount := disputePlan(contractID, milestone, refund) approve(cur, pid, p) before := escrowBalance() escrow.ResolveDispute(cross(cur), contractID, milestone, refund) if paid := before - escrowBalance(); paid < 0 || paid > amount { panic("memba_bridge: escrow paid out more than the milestone") } } // disputePlan returns the approval and the milestone's amount. func disputePlan(contractID string, milestone int, refund bool) (plan, int64) { c := escrow.GetGovernanceContract(contractID) if !c.Exists || milestone < 0 || milestone >= c.Count { panic("memba_bridge: no such contract milestone") } m := c.Milestones[milestone] if m.Status != "disputed" { panic("memba_bridge: milestone is " + m.Status + ", not disputed") } args := daoauth.New().Str(contractID).Int(int64(milestone)).Bool(refund). Str(m.Status).Str(m.PreDisputeStatus).Int(m.Amount).Int(m.DisputedAt) if !refund { terms := escrow.GetGovernanceFeeTerms() args.Int(terms.EffectiveBPS).Str(string(terms.EffectiveTreasury)) } return newPlan(appEscrow, "ResolveDispute", args, appEscrow+"/c/"+contractID+"/m/"+strconv.Itoa(milestone), gov.Financial), m.Amount } func escrowBalance() int64 { // One denom only: junk denoms sent to escrow cost nothing to read past. return banker.NewReadonlyBanker().GetCoin(chain.PackageAddress("gno.land/r/samcrew/"+appEscrow), "ugnot") } // ProposeFeeRecipient stages escrow's fallback fee recipient, used only when // market_config's treasury is unusable. func ProposeFeeRecipient(cur realm, pid uint64, addr address) { approve(cur, pid, feeRecipientPlan(addr)) escrow.ProposeFeeRecipient(cross(cur), addr) } func feeRecipientPlan(addr address) plan { return newPlan(appEscrow, "ProposeFeeRecipient", daoauth.New().Addr(arg(addr)). Str(escrow.GetFeeRecipient()).Str(escrow.GetPendingFeeRecipient()), appEscrow, gov.Critical) } func CancelFeeRecipient(cur realm, pid uint64) { approve(cur, pid, cancelFeeRecipientPlan()) escrow.CancelFeeRecipientProposal(cross(cur)) } func cancelFeeRecipientPlan() plan { pending := escrow.GetPendingFeeRecipient() if pending == "" { panic("memba_bridge: no fee recipient staged") } return newPlan(appEscrow, "CancelFeeRecipient", daoauth.New().Str(pending), appEscrow, gov.Financial) } // ── Reviews ───────────────────────────────────────────────────────────── func HideReview(cur realm, pid uint64, id uint64) { moderate(cur, pid, "HideReview", id) } func HideComment(cur realm, pid uint64, id uint64) { moderate(cur, pid, "HideComment", id) } // Unhide shows an item again and dismisses its flags. func Unhide(cur realm, pid uint64, id uint64) { moderate(cur, pid, "Unhide", id) } // moderate binds the item's kind and visibility; Unhide, which shows the // item again, also binds its content, so an author editing it can neither // void a hide vote nor slip new text past an unhide vote. func moderate(cur realm, pid uint64, op string, id uint64) { approve(cur, pid, moderationPlan(op, id)) switch op { case "HideReview": reviews.HideReview(cross(cur), id) case "HideComment": reviews.HideComment(cross(cur), id) default: reviews.Unhide(cross(cur), id) } } func moderationPlan(op string, id uint64) plan { s := reviews.GetModerationState(id) switch { case s.ID == 0: // Ids are predictable: an approval for an item not posted yet would hide whatever takes the id. panic("memba_bridge: no review or comment " + strconv.FormatUint(id, 10)) case op == "HideReview" && !s.Review: panic("memba_bridge: item " + strconv.FormatUint(id, 10) + " is a comment, not a review") case op == "HideComment" && s.Review: panic("memba_bridge: item " + strconv.FormatUint(id, 10) + " is a review, not a comment") case op != "Unhide" && (s.Hidden || s.Deleted): panic("memba_bridge: already hidden or deleted") case op == "Unhide" && !s.Hidden && !s.Flagged: panic("memba_bridge: neither hidden nor flagged") } // The author and creation time pin the item itself. args := daoauth.New().Uint(id).Bool(s.Review).Bool(s.Hidden).Bool(s.Deleted).Addr(s.Author).Int(s.CreatedAt) if op == "Unhide" { // The text is bound by its hash and edit height. Unhide also dismisses // flags; the flagged bit is bound, not a count (reviews_v2 exposes none), // so a flag added after the vote to an item already flagged is // dismissed with the others. args.Str(s.BodyHash).Int(s.EditedAt).Bool(s.Flagged) } return newPlan(appReviews, op, args, appReviews+"/i/"+strconv.FormatUint(id, 10), gov.Routine) } // ── Quests ────────────────────────────────────────────────────────────── // SetQuestSigner rotates the key every quest voucher is checked against. func SetQuestSigner(cur realm, pid uint64, pubKeyHex string) { approve(cur, pid, signerPlan(pubKeyHex)) quest.SetSigner(cross(cur), pubKeyHex) } func signerPlan(pubKeyHex string) plan { if len(pubKeyHex) != 64 || strings.Trim(pubKeyHex, "0123456789abcdef") != "" || pubKeyHex == quest.GetSigner() { panic("memba_bridge: a new signer is 64 lowercase hex digits") } return newPlan(appQuest, "SetSigner", daoauth.New().Str(pubKeyHex).Str(quest.GetSigner()), appQuest, gov.Critical) } // ── Channels and feedback membership ──────────────────────────────────── // AddMember adds a new member; it never rewrites an existing member's roles. func AddMember(cur realm, pid uint64, app string, addr address, roles string) { member(cur, pid, app, "AddMember", addr, roles) } func RemoveMember(cur realm, pid uint64, app string, addr address) { member(cur, pid, app, "RemoveMember", addr, "") } func SetRoles(cur realm, pid uint64, app string, addr address, roles string) { member(cur, pid, app, "SetRoles", addr, roles) } func member(cur realm, pid uint64, app, op string, who address, roles string) { approve(cur, pid, memberPlan(app, op, who, roles)) switch { case app == appChannels && op == "AddMember": channels.AddMember(cross(cur), who, roles) case app == appChannels && op == "RemoveMember": channels.RemoveMember(cross(cur), who) case app == appChannels: channels.UpdateMemberRoles(cross(cur), who, roles) case op == "AddMember": feedback.AddMember(cross(cur), who, roles) case op == "RemoveMember": feedback.RemoveMember(cross(cur), who) default: feedback.UpdateMemberRoles(cross(cur), who, roles) } } func memberPlan(app, op string, who address, roles string) plan { if op == "RemoveMember" { canonical(who) // a stale membership of a protected realm can be removed } else { arg(who) } var revision uint64 var current string switch app { case appChannels: revision, current = channels.GetMembershipRevision(), channels.GetMemberRoles(who) case appFeedback: revision, current = feedback.GetMembershipRevision(), feedback.GetMemberRoles(who) default: panic("memba_bridge: " + app + " has no members") } if (op == "AddMember") != (current == "") || (op != "RemoveMember" && !validRoles(roles)) { panic("memba_bridge: add a non-member, change a member, roles an ordered subset of admin,dev,ops,member") } return newPlan(app, op, daoauth.New().Addr(who).Str(roles).Uint(revision).Str(current), app, gov.Critical) } // validRoles accepts a non-empty, ordered, duplicate-free subset of // admin,dev,ops,member. func validRoles(r string) bool { names := []string{"admin", "dev", "ops", "member"} next := 0 for _, x := range strings.Split(r, ",") { for next < len(names) && names[next] != x { next++ } if next == len(names) { return false } next++ } return true } // CreateChannel adds a channel. Channels are permanent (at most 20). func CreateChannel(cur realm, pid uint64, app, name, description, ctype string) { approve(cur, pid, channelPlan(app, name, description, ctype)) if app == appChannels { channels.CreateChannel(cross(cur), name, description, ctype) } else { feedback.CreateChannel(cross(cur), name, description, ctype) } } func channelPlan(app, name, description, ctype string) plan { if ctype != "text" && ctype != "announcements" && ctype != "readonly" || description == "" || len(description) > 200 || !daoauth.ValidText(description) { panic("memba_bridge: type text, announcements or readonly; description 1-200 printable ASCII characters") } var count int var exists bool switch app { case appChannels: count, exists, _, _, _, _ = channels.GetChannelState(name) case appFeedback: count, exists, _, _, _, _ = feedback.GetChannelState(name) default: panic("memba_bridge: " + app + " has no channels") } if exists { panic("memba_bridge: channel exists") } if !channelName(name) || count >= 20 { panic("memba_bridge: a channel name is 1-50 of a-z, 0-9 or -, and an app holds at most 20 channels") } return newPlan(app, "CreateChannel", daoauth.New().Str(name).Str(description).Str(ctype).Int(int64(count)), app, gov.Critical) } // channelName is the apps' own channel-name rule. func channelName(name string) bool { if name == "" || len(name) > 50 { return false } return strings.Trim(name, "abcdefghijklmnopqrstuvwxyz0123456789-") == "" }
  4. #4bridge.gno
  5. #5// Package memba_bridge_v1 governs the ten Memba application realms published // before memba_gov. It becomes each app's admin through the app's own // two-step handover, so the apps' code does not change, and turns memba_gov // approvals into the app's owner-only calls. // // Every governed entrypoint is a direct call by a seated member. It reads the // app's live pre-state, encodes it with the call's arguments and the app's // tenure (how many times the bridge has accepted it), and consumes exactly // that approval from memba_gov before calling the app: an approval voted on // another state, app, action, argument or tenure never matches. Action ids // are "<app>.<Op>"; the invalidation scope is the app, or an object of it // where actions are independent. // // Accept, ExpirePause and the reads need no vote. EmergencyPause needs a // seated member, no vote, and expires by itself. Approval tells a proposer // exactly what an entrypoint call would consume, from the same code. package memba_bridge_v1 import ( "chain" "strconv" "time" "gno.land/p/samcrew/daoauth" gov "gno.land/r/samcrew/memba_gov" escrow "gno.land/r/samcrew/escrow_v4" badges "gno.land/r/samcrew/gnobuilders_badges_v2" appstore "gno.land/r/samcrew/memba_appstore_v3" arcade "gno.land/r/samcrew/memba_arcade_leaderboard_v1" channels "gno.land/r/samcrew/memba_dao_channels_v2" feed "gno.land/r/samcrew/memba_feed_v1" feedback "gno.land/r/samcrew/memba_feedback_v2" market "gno.land/r/samcrew/memba_market_config" quest "gno.land/r/samcrew/memba_quest_attestation_v1" reviews "gno.land/r/samcrew/memba_reviews_v2" ) const ( appMarket = "memba_market_config" appEscrow = "escrow_v4" appAppstore = "memba_appstore_v3" appReviews = "memba_reviews_v2" appQuest = "memba_quest_attestation_v1" appArcade = "memba_arcade_leaderboard_v1" appBadges = "gnobuilders_badges_v2" appFeed = "memba_feed_v1" appChannels = "memba_dao_channels_v2" appFeedback = "memba_feedback_v2" ) var ( self = chain.PackageAddress("gno.land/r/samcrew/memba_bridge_v1") apps = []string{appMarket, appEscrow, appAppstore, appReviews, appQuest, appArcade, appBadges, appFeed, appChannels, appFeedback} // Addresses no governed argument may name: fees sent there are locked // for good, and a role given to one of them is unusable. protected = map[address]bool{} tenure = map[string]uint64{} // app -> times the bridge accepted it pausedUntil = map[string]time.Time{} // app -> end of the bridge's pause episodes = map[string]uint64{} // app -> pauses the bridge started lastPause = map[string]time.Time{} // member id -> last emergency pause ) func init() { for _, path := range []string{"memba_bridge_v1", "memba_gov", "memba_dao", "escrow_v3"} { protected[chain.PackageAddress("gno.land/r/samcrew/"+path)] = true } for _, app := range apps { protected[chain.PackageAddress("gno.land/r/samcrew/"+app)] = true } } // ── Authorisation ─────────────────────────────────────────────────────── // direct returns the account that called the bridge directly. func direct(cur realm) address { if !cur.IsCurrent() || !cur.Previous().IsUserCall() { panic("memba_bridge: call directly from your account") } return cur.Previous().Address() } // A plan is the approval one governed call needs: action app.op, args // (completed by the app's tenure, which voids approvals voted while the app // was away), the invalidation scope and the minimum class. type plan struct { app, op string args *daoauth.Args scope string class int } // newPlan refuses an approval for an app the bridge does not govern now: no // call could consume it. func newPlan(app, op string, args *daoauth.Args, scope string, class int) plan { if !governs(app) { panic("memba_bridge: the bridge does not govern " + app) } return plan{app, op, args, scope, class} } func (p plan) voted() string { return p.args.Uint(tenure[p.app]).String() } // approve consumes proposal pid for p; memba_gov checks the executor, the // bridge's direct caller, is a seated member. func approve(cur realm, pid uint64, p plan) { executor := direct(cur) gov.Consume(cross(cur), pid, p.app+"."+p.op, p.voted(), p.scope, p.class, executor) } // Approval returns, as JSON, the proposal a governed entrypoint call would // consume if made now: target this realm, with the action, args, scope and // minimum class given. call is the entrypoint's name and its arguments after // pid, daoauth-encoded in order (strings s:, addresses a:, integers i: or // u:, booleans b:). It refuses a call its entrypoint would refuse before // voting, and holds only while the state it binds is unchanged. func Approval(call string) string { c := parseCall(call) var p plan switch name := c.str(); name { case "TransferAdmin": p = transferPlan(c.str(), c.addr()) case "CancelTransfer": p = cancelPlan(c.str()) case "SetPause": p = pausePlan(c.str(), c.i64()) case "Grant", "Revoke": p = rolePlan(c.str(), c.addr(), name == "Grant") case "Curate": p = curatePlan(c.str(), c.str(), c.str()) case "SetRegistrationFee": p = registrationFeePlan(c.i64()) case "SetTreasury": p = treasuryPlan(c.str(), c.addr()) case "SetFee": p = feePlan(c.str(), c.i64()) case "ResolveDispute": p, _ = disputePlan(c.str(), int(c.i64()), c.flag()) case "ProposeFeeRecipient": p = feeRecipientPlan(c.addr()) case "CancelFeeRecipient": p = cancelFeeRecipientPlan() case "HideReview", "HideComment", "Unhide": p = moderationPlan(name, c.u64()) case "SetQuestSigner": p = signerPlan(c.str()) case "AddMember", "SetRoles": p = memberPlan(c.str(), name, c.addr(), c.str()) case "RemoveMember": p = memberPlan(c.str(), name, c.addr(), "") case "CreateChannel": p = channelPlan(c.str(), c.str(), c.str(), c.str()) default: panic("memba_bridge: no governed entrypoint " + name) } c.done() return `{"target":"gno.land/r/samcrew/memba_bridge_v1","action":` + quote(p.app+"."+p.op) + `,"args":` + quote(p.voted()) + `,"scope":` + quote(p.scope) + `,"class":` + strconv.Itoa(p.class) + `}` } // call reads an Approval call's fields in order, each with its tag. type call struct{ f []daoauth.Field } func parseCall(s string) *call { f, err := daoauth.Parse(s) if err != nil { panic(err.Error()) } return &call{f} } func (c *call) next(tag byte) string { if len(c.f) == 0 || c.f[0].Tag != tag { panic("memba_bridge: call argument " + string(tag) + ": expected") } v := c.f[0].Value c.f = c.f[1:] return v } // daoauth.Parse has checked each value is canonical for its tag. func (c *call) str() string { return c.next('s') } func (c *call) addr() address { return address(c.next('a')) } func (c *call) i64() int64 { n, _ := strconv.ParseInt(c.next('i'), 10, 64) return n } func (c *call) u64() uint64 { n, _ := strconv.ParseUint(c.next('u'), 10, 64) return n } func (c *call) flag() bool { return c.next('b') == "1" } func (c *call) done() { if len(c.f) != 0 { panic("memba_bridge: too many call arguments") } } // quote returns s, printable ASCII, as a JSON string with < > & escaped. func quote(s string) string { b := make([]byte, 0, 6*len(s)+2) b = append(b, '"') for i := 0; i < len(s); i++ { switch c := s[i]; c { case '"', '\\': b = append(b, '\\', c) case '<', '>', '&': b = append(b, `\u00`...) b = append(b, "0123456789abcdef"[c>>4], "0123456789abcdef"[c&0xf]) default: b = append(b, c) } } return string(append(b, '"')) } func checkApp(app string) { for _, a := range apps { if a == app { return } } panic("memba_bridge: unknown app " + app) } // arg refuses a non-canonical or protected address as a recipient of a role, // membership, admin or fees. func arg(a address) address { if protected[a] { panic("memba_bridge: address must not be a governance or app realm") } return canonical(a) } // canonical refuses an address the chain would never give a caller. func canonical(a address) address { if !daoauth.ValidAddress(a) { panic("memba_bridge: address must be lowercase bech32") } return a } // ── Admin handover (every app) ────────────────────────────────────────── // Accept takes the admin role the 2-of-3 nominated the bridge for. It needs // no vote: the nomination is the decision. A paused app is refused: only a // pause the bridge started can be ended by its rules. func Accept(cur realm, app string) { checkApp(app) if hasPause(app) && isPaused(app) { panic("memba_bridge: unpause " + app + " before handing it over") } switch app { case appMarket: market.AcceptAdmin(cross(cur)) case appEscrow: escrow.AcceptOwnership(cross(cur)) case appAppstore: appstore.AcceptOwnership(cross(cur)) case appReviews: reviews.AcceptOwnership(cross(cur)) case appQuest: quest.AcceptOwnership(cross(cur)) case appArcade: arcade.AcceptOwnership(cross(cur)) case appBadges: badges.AcceptOwnership(cross(cur)) case appFeed: feed.AcceptOwnership(cross(cur)) case appChannels: channels.AcceptOwnership(cross(cur)) case appFeedback: feedback.AcceptOwnership(cross(cur)) } tenure[app]++ delete(pausedUntil, app) // a record from an earlier tenure never applies } // TransferAdmin stages to as the app's next admin: the 2-of-3, a successor // bridge, or anyone the DAO names. The recipient accepts by its own call. func TransferAdmin(cur realm, pid uint64, app string, to address) { approve(cur, pid, transferPlan(app, to)) switch app { case appMarket: market.TransferAdmin(cross(cur), to) case appEscrow: escrow.TransferOwnership(cross(cur), to) case appAppstore: appstore.TransferOwnership(cross(cur), to) case appReviews: reviews.TransferOwnership(cross(cur), to) case appQuest: quest.TransferOwnership(cross(cur), to) case appArcade: arcade.TransferOwnership(cross(cur), to) case appBadges: badges.TransferOwnership(cross(cur), to) case appFeed: feed.TransferOwnership(cross(cur), to) case appChannels: channels.ProposeOwner(cross(cur), to) case appFeedback: feedback.TransferOwnership(cross(cur), to) } } func transferPlan(app string, to address) plan { checkApp(app) return newPlan(app, "TransferAdmin", daoauth.New().Addr(arg(to)).Str(pendingAdmin(app)), app, gov.Critical) } // CancelTransfer withdraws a staged admin before the recipient accepts. func CancelTransfer(cur realm, pid uint64, app string) { approve(cur, pid, cancelPlan(app)) switch app { case appMarket: market.CancelAdminTransfer(cross(cur)) case appEscrow: escrow.CancelOwnershipTransfer(cross(cur)) case appAppstore: appstore.CancelOwnershipTransfer(cross(cur)) case appReviews: reviews.CancelOwnershipTransfer(cross(cur)) case appQuest: quest.CancelOwnershipTransfer(cross(cur)) case appArcade: arcade.CancelOwnershipTransfer(cross(cur)) case appBadges: badges.CancelOwnershipTransfer(cross(cur)) case appFeed: feed.CancelOwnershipTransfer(cross(cur)) case appChannels: channels.CancelPendingOwner(cross(cur)) case appFeedback: feedback.CancelOwnershipTransfer(cross(cur)) } } func cancelPlan(app string) plan { checkApp(app) pending := pendingAdmin(app) if pending == "" { panic("memba_bridge: nothing staged to cancel for " + app) } return newPlan(app, "CancelTransfer", daoauth.New().Str(pending), app, gov.Financial) } // governs reports whether the bridge is app's admin now. func governs(app string) bool { var admin string switch app { case appMarket: admin = string(market.GetAdmin()) case appEscrow: admin = escrow.GetAdmin() case appAppstore: admin = string(appstore.GetOwner()) case appReviews: admin = reviews.GetModerator() case appQuest: admin = quest.GetOwner() case appArcade: admin = string(arcade.GetOwner()) case appBadges: admin = string(badges.GetOwner()) case appFeed: admin = string(feed.GetOwner()) case appChannels: admin = string(channels.GetOwner()) case appFeedback: admin = string(feedback.GetOwner()) default: return false } return admin == string(self) } func pendingAdmin(app string) string { switch app { case appMarket: return string(market.GetPendingAdmin()) case appEscrow: return escrow.GetPendingAdmin() case appAppstore: return string(appstore.GetPendingOwner()) case appReviews: return reviews.GetPendingModerator() case appQuest: return quest.GetPendingOwner() case appArcade: return string(arcade.GetPendingOwner()) case appBadges: return string(badges.GetPendingOwner()) case appFeed: return string(feed.GetPendingOwner()) case appChannels: return string(channels.GetPendingOwner()) } return string(feedback.GetPendingOwner()) } // ── Pause (escrow, App Store, arcade, badges, feed, channels, feedback) ── // EmergencyPause pauses app for 7 days, without a vote. Each member may use // it once per 30 days, so one key cannot keep an app closed. func EmergencyPause(cur realm, app string) { id := gov.PersonOf(direct(cur)) if id == "" { panic("memba_bridge: not a seated member") } if !governs(app) { panic("memba_bridge: the bridge does not govern " + app) } // A paused app is refused first, and costs no allowance: an emergency // pause must never shorten a longer pause members voted. if isPaused(app) { panic("memba_bridge: already paused") } now := time.Now() if last, ok := lastPause[id]; ok && now.Before(last.Add(PauseAllowance)) { panic("memba_bridge: one emergency pause per member per 30 days") } setPaused(cur, app, true) pausedUntil[app] = now.Add(EmergencyPauseFor) episodes[app]++ lastPause[id] = now } // SetPause ends the bridge's pause of app now (until = 0) or keeps app // paused until the unix time until, at most 30 days ahead. The approval names // the pause episode, so it never ends a later pause. func SetPause(cur realm, pid uint64, app string, until int64) { approve(cur, pid, pausePlan(app, until)) if until == 0 { if isPaused(app) { setPaused(cur, app, false) } delete(pausedUntil, app) return } if !isPaused(app) { setPaused(cur, app, true) episodes[app]++ } pausedUntil[app] = time.Unix(until, 0) } func pausePlan(app string, until int64) plan { now := time.Now() if end := time.Unix(until, 0); until != 0 && (!end.After(now) || end.After(now.Add(MaxPauseAhead))) { panic("memba_bridge: pause end must be within the next 30 days") } // Governance first: an app the bridge does not govern says so before its pause is read. p := newPlan(app, "SetPause", daoauth.New(), app, gov.Financial) p.args.Int(until).Bool(isPaused(app)).Uint(episodes[app]) return p } // ExpirePause lets anyone end the bridge's pause of app once it is over. A // SetPause that keeps app paused longer must execute before then. Once the // bridge no longer governs app, its record no longer applies: anyone drops // it, and the app, now its new admin's, is left as it is. func ExpirePause(cur realm, app string) { end, ok := pausedUntil[app] if ok && !governs(app) { delete(pausedUntil, app) return } if !ok || time.Now().Before(end) { panic("memba_bridge: no expired pause for " + app) } delete(pausedUntil, app) if isPaused(app) { setPaused(cur, app, false) } } func hasPause(app string) bool { return app != appMarket && app != appReviews && app != appQuest } func isPaused(app string) bool { switch app { case appEscrow: return escrow.IsPaused() case appAppstore: return appstore.IsPaused() case appArcade: return arcade.IsPaused() case appBadges: return badges.IsPaused() case appFeed: return feed.IsPaused() case appChannels: return channels.IsPaused() case appFeedback: return feedback.IsPaused() } panic("memba_bridge: " + app + " has no pause") } func setPaused(cur realm, app string, on bool) { switch { case app == appEscrow && on: escrow.Pause(cross(cur)) case app == appEscrow: escrow.Unpause(cross(cur)) case app == appAppstore: appstore.Pause(cross(cur), on) case app == appArcade: arcade.Pause(cross(cur), on) case app == appBadges && on: badges.Pause(cross(cur)) case app == appBadges: badges.Unpause(cross(cur)) case app == appFeed && on: feed.PauseRealm(cross(cur)) case app == appFeed: feed.UnpauseRealm(cross(cur)) case app == appChannels && on: channels.PauseRealm(cross(cur)) case app == appChannels: channels.UnpauseRealm(cross(cur)) case app == appFeedback && on: feedback.PauseRealm(cross(cur)) case app == appFeedback: feedback.UnpauseRealm(cross(cur)) } } func unixOf(t time.Time) int64 { if t.IsZero() { return 0 } return t.Unix() } // ── Reads ─────────────────────────────────────────────────────────────── // PausedUntil returns the unix end of the bridge's pause of app, or 0 (also // once the bridge no longer governs app). func PausedUntil(app string) int64 { if !governs(app) { return 0 } return unixOf(pausedUntil[app]) } // PauseEpisode returns how many pauses of app the bridge has started; a // SetPause approval binds it. func PauseEpisode(app string) uint64 { return episodes[app] } // Tenure returns how many times the bridge has accepted app; every approval // for app binds it as its last argument. func Tenure(app string) uint64 { return tenure[app] } func Render(_ string) string { out := "# Memba bridge v1\n\nGoverns the ten Memba apps for memba_gov. Bridge pauses:\n\n" n := 0 for _, app := range apps { if end, ok := pausedUntil[app]; ok && governs(app) { out += "- " + app + " until unix " + strconv.FormatInt(end.Unix(), 10) + "\n" n++ } } if n == 0 { out += "None.\n" } return out }
  6. #6durations.gno
  7. #7package memba_bridge_v1 import "time" // The pause durations, in their own file so a node fixture, which cannot // wait days, can replace this file alone; TestProductionDurations pins them. const ( EmergencyPauseFor = 7 * 24 * time.Hour // escrow keeps exits closed this long PauseAllowance = 30 * 24 * time.Hour // one emergency pause per member per period MaxPauseAhead = 30 * 24 * time.Hour )
  8. #8gnomod.toml
  9. #9module = "gno.land/r/samcrew/memba_bridge_v1" gno = "0.9"
  10. #10/gno.MemPackageType
  11. #11 MPUserAll

Result log

msg:0,success:true,log:,events:[]

← Back to block 652,693