1package dao23import (4 "errors"5 "strings"67 "gno.land/p/nt/bptree/v0"8 "gno.land/p/nt/seqid/v0"9)1011type ProposalID int641213func (pid ProposalID) String() string {14 return seqid.ID(pid).String()15}1617// VoteOption is the limited voting option for a DAO proposal18// New govDAOs can create their own VoteOptions if needed in the19// future.20type VoteOption string2122const (23 AbstainVote VoteOption = "ABSTAIN" // Side is not chosen24 YesVote VoteOption = "YES" // Proposal should be accepted25 NoVote VoteOption = "NO" // Proposal should be rejected26)2728type VoteRequest struct {29 Option VoteOption30 ProposalID ProposalID31 Metadata interface{}32}3334func NewVoteRequest(option VoteOption, proposalID ProposalID) VoteRequest {35 return VoteRequest{36 Option: option,37 ProposalID: proposalID,38 }39}4041func NewVoteRequestWithMetadata(option VoteOption, proposalID ProposalID, metadata interface{}) VoteRequest {42 return VoteRequest{43 Option: option,44 ProposalID: proposalID,45 Metadata: metadata,46 }47}4849func NewProposalRequest(title string, description string, executor Executor) ProposalRequest {50 return ProposalRequest{51 title: title,52 description: description,53 executor: executor,54 }55}5657func NewProposalRequestWithFilter(title string, description string, executor Executor, filter Filter) ProposalRequest {58 return ProposalRequest{59 title: title,60 description: description,61 executor: executor,62 filter: filter,63 }64}6566type Filter interface{}6768type ProposalRequest struct {69 title string70 description string71 executor Executor72 filter Filter73}7475func (p *ProposalRequest) Title() string {76 return p.title77}7879func (p *ProposalRequest) Description() string {80 return p.description81}8283func (p *ProposalRequest) Filter() Filter {84 return p.filter85}8687type Proposal struct {88 author address8990 title string91 description string9293 executor Executor94 allowedDAOs []string95}9697func (p *Proposal) Author() address {98 return p.author99}100101func (p *Proposal) Title() string {102 return p.title103}104105func (p *Proposal) Description() string {106 return p.description107}108109func (p *Proposal) ExecutorString() string {110 if p.executor != nil {111 return p.executor.String()112 }113114 return ""115}116117func (p *Proposal) ExecutorCreationRealm() string {118 if p.executor != nil {119 return p.executor.CreationRealm()120 }121122 return ""123}124125func (p *Proposal) AllowedDAOs() []string {126 return append([]string(nil), p.allowedDAOs...)127}128129type Proposals struct {130 seq seqid.ID131 *bptree.BPTree // *bptree.BPTree[ProposalID]*Proposal132}133134func NewProposals() *Proposals {135 return &Proposals{BPTree: bptree.NewBPTree32()}136}137138func (ps *Proposals) SetProposal(p *Proposal) ProposalID {139 pid := ProposalID(int64(ps.seq))140 updated := ps.Set(pid.String(), p)141 if updated {142 panic("fatal error: Override proposals is not allowed")143 }144 ps.seq = ps.seq.Next()145 return pid146}147148func (ps *Proposals) GetProposal(pid ProposalID) *Proposal {149 pv := ps.Get(pid.String())150 if pv == nil {151 return nil152 }153154 return pv.(*Proposal)155}156157type Executor interface {158 Execute(cur realm) error159 String() string160 CreationRealm() string161}162163// NewSimpleExecutor constructs an Executor whose creationRealm is captured164// from rlm.PkgPath() at construction time. The IsCurrent() check rejects165// stale or stashed realm values so the captured value is the authentic166// caller realm. creationRealm is display-only (rendered as "Executor167// created in: ..." in proposal listings) — no auth gate downstream.168func NewSimpleExecutor(_ int, rlm realm, callback func(realm) error, description string) *SimpleExecutor {169 if !rlm.IsCurrent() {170 panic("NewSimpleExecutor: rlm is not the caller's live cur (stale capture or sibling frame)")171 }172 if callback == nil {173 panic("executor callback must not be nil")174 }175176 return &SimpleExecutor{177 callback: callback,178 desc: description,179 creationRealm: rlm.PkgPath(),180 }181}182183// SimpleExecutor implements the Executor interface using184// a callback function and a description string.185type SimpleExecutor struct {186 callback func(realm) error187 desc string188 creationRealm string189}190191// proxyPkgPath is this package: the realm whose frame Execute mints for a192// callback, and therefore the only realm entitled to invoke one.193const proxyPkgPath = "gno.land/r/gov/dao"194195// Execute runs the proposal's callback. Invocable only from this proxy.196//197// SECURITY: Execute is a CROSSING method declared here, so invoking it mints198// a gno.land/r/gov/dao frame for the callback — `cur.Previous()` inside the199// callback is this path whoever called. That is a capability: realms gate on200// it (authz.NewContractAuthority("gno.land/r/gov/dao") + DoByPrevious,201// ownable, anything reading Previous().Address()). Ungated, any realm could202// wrap a reachable `func(realm) error` — including an ordinary exported203// entrypoint of the victim — and run it with governance's identity, with no204// proposal id minted, so nothing to render, audit or deny afterwards.205//206// NOT InAllowedDAOs: that is SafeExecutor's bug. It holds the impl path while207// an executor's caller is the proxy, so it rejects the approved route and208// fails open while empty. Exact path plus subpackages, because209// impl.ExecuteProposal is non-crossing (Previous() is the proxy exactly), so210// a bare prefix would reject every real proposal.211//212// Gates the executor object's INVOCATION only. Keeping a privileged closure,213// or an exported entrypoint shaped like `func(realm) error`, out of reach214// stays the consumer's job — see r/gnops/valopers/admin.gno215// and p/moul/authz's NewContractAuthority godoc.216func (e *SimpleExecutor) Execute(cur realm) error {217 // IsCurrent first, as every other gate here does: otherwise Previous() is218 // read from whatever realm value the caller threaded in.219 if !cur.IsCurrent() {220 return errors.New("execution denied: cur is not the caller's live realm")221 }222 if prev := cur.Previous().PkgPath(); prev != proxyPkgPath &&223 !strings.HasPrefix(prev, proxyPkgPath+"/") {224 return errors.New("execution denied: executors are only invocable by " + proxyPkgPath)225 }226227 // Check if executor was created using the constructor func228 if e.callback == nil {229 return nil230 }231232 return e.callback(cross(cur))233}234235func (e *SimpleExecutor) String() string {236 return e.desc237}238239func (e *SimpleExecutor) CreationRealm() string {240 return e.creationRealm241}242243func NewSafeExecutor(e Executor) *SafeExecutor {244 return &SafeExecutor{245 e: e,246 }247}248249// SafeExecutor wraps an Executor to only allow its execution250// by allowed govDAOs.251type SafeExecutor struct {252 e Executor253}254255func (e *SafeExecutor) Execute(cur realm) error {256 // IsCurrent first, matching every other allowlist gate in this tree257 // (proxy.go's UpdateImpl, treasury, memberstore). Without it this method258 // trusts whatever realm value it is handed, so a caller threading a stale259 // or sibling-frame cur would have its Previous() read from that value260 // rather than from the live frame.261 //262 // NewSafeExecutor has no call sites, so this type is currently dead code.263 // Live proposal execution goes through the Executor interface to264 // SimpleExecutor.Execute, which now gates on the invoking realm being the265 // proxy -- deliberately NOT on InAllowedDAOs, which holds the impl path266 // and therefore rejects the approved route and fails open while empty.267 // That is this method's bug; see SimpleExecutor.Execute.268 if !cur.IsCurrent() {269 return errors.New("execution denied: cur is not the caller's live realm")270 }271 // Verify the caller is an adequate Realm272 if !InAllowedDAOs(cur.Previous().PkgPath()) {273 return errors.New("execution only allowed by validated govDAOs")274 }275276 return e.e.Execute(cross(cur))277}278279func (e *SafeExecutor) String() string {280 return e.e.String()281}282283func (e *SafeExecutor) CreationRealm() string {284 return e.e.CreationRealm()285}286287// DAO is the govDAO implementation interface. All mutating/auth-gated288// methods take rlm as their realm-typed parameter in the second position289// (the `_ int, rlm realm` non-crossing form): callers thread the proxy's290// cur as data without forcing a realm transition, so the impl's existing291// unsafe.CurrentRealm()-based auth gates (isValidCall, memberstore.Get)292// continue to see the proxy realm. Render stays unchanged.293type DAO interface {294 // PreCreateProposal is called just before creating a new Proposal295 // It is intended to be used to get the address of the proposal, that296 // may vary depending on the DAO implementation, and to validate that297 // the requester is allowed to do a proposal298 PreCreateProposal(_ int, rlm realm, r ProposalRequest) (address, error)299300 // PostCreateProposal is called after creating the Proposal. It is301 // intended to be used as a way to store a new proposal status, that302 // depends on the actuall govDAO implementation303 PostCreateProposal(_ int, rlm realm, r ProposalRequest, pid ProposalID)304305 // VoteOnProposal will send a petition to vote for a specific proposal306 // to the actual govDAO implementation307 VoteOnProposal(_ int, rlm realm, r VoteRequest) error308309 // PreExecuteProposal is called when someone is trying to execute a proposal by ID.310 // Is intended to be used to validate who can trigger the proposal execution.311 PreExecuteProposal(_ int, rlm realm, pid ProposalID) (bool, error)312313 // ExecuteProposal executes the proposal executor and on error changes proposal314 // status to denied with the error message being the denial reason.315 // It returns the executor error when it fails.316 ExecuteProposal(_ int, rlm realm, pid ProposalID, e Executor) error317318 // Render will return a human-readable string in markdown format that319 // will be used to show new data through the dao proxy entrypoint.320 // Crossing: the chain query layer auto-injects .cur, and321 // implementations forward cur to internal rlm-aware helpers (mux322 // RenderRlm + downstream cross(rlm) reads).323 Render(cur realm, pkgpath string, path string) string324}325326type UpdateRequest struct {327 DAO DAO328 AllowedDAOs []string329}330331// NewUpdateRequest copies allowedDAOs into a fresh slice owned by332// /r/gov/dao. Under the storage=authority model, if we stored the333// caller-passed slice directly, the base ArrayValue would retain334// PkgID = caller_realm: storage rent would attribute to caller, and335// /r/gov/dao could not mutate (e.g. append to) its own copy without336// a DidUpdate panic. The internal copy ensures the UpdateRequest337// and its AllowedDAOs both live entirely in /r/gov/dao's authority.338func NewUpdateRequest(d DAO, allowedDAOs []string) UpdateRequest {339 cp := make([]string, len(allowedDAOs))340 copy(cp, allowedDAOs)341 return UpdateRequest{342 DAO: d,343 AllowedDAOs: cp,344 }345}346AllowedDAOs() []string
CreateProposal(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}, r struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}) (int64, interface {Error func() string})
ExecuteOrRejectProposal(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}, pid int64) bool
ExecuteProposal(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}, pid int64) bool
GetProposal(pid int64) (*gno.land/r/gov/dao.Proposal, interface {Error func() string})
InAllowedDAOs(pkg string) bool
MustCreateProposal(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}, r struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}) int64
MustGetProposal(pid int64) *gno.land/r/gov/dao.Proposal
MustVoteOnProposal(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}, r struct{Option gno.land/r/gov/dao.VoteOption; ProposalID gno.land/r/gov/dao.ProposalID; Metadata interface {}})
MustVoteOnProposalSimple(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}, pid int64, option string)
NewProposalRequest(title string, description string, executor interface {CreationRealm func() string; Execute func(.uverse.realm) .uverse.error; String func() string}) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
NewProposalRequestWithFilter(title string, description string, executor interface {CreationRealm func() string; Execute func(.uverse.realm) .uverse.error; String func() string}, filter interface {}) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
NewProposals() *gno.land/r/gov/dao.Proposals
NewSafeExecutor(e interface {CreationRealm func() string; Execute func(.uverse.realm) .uverse.error; String func() string}) *gno.land/r/gov/dao.SafeExecutor
NewSimpleExecutor(int, rlm 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}, callback func(.uverse.realm) .uverse.error, description string) *gno.land/r/gov/dao.SimpleExecutor
NewUpdateRequest(d interface {ExecuteProposal func(int, .uverse.realm, gno.land/r/gov/dao.ProposalID, gno.land/r/gov/dao.Executor) .uverse.error; PostCreateProposal func(int, .uverse.realm, gno.land/r/gov/dao.ProposalRequest, gno.land/r/gov/dao.ProposalID); PreCreateProposal func(int, .uverse.realm, gno.land/r/gov/dao.ProposalRequest) (.uverse.address, .uverse.error); PreExecuteProposal func(int, .uverse.realm, gno.land/r/gov/dao.ProposalID) (bool, .uverse.error); Render func(.uverse.realm, string, string) string; VoteOnProposal func(int, .uverse.realm, gno.land/r/gov/dao.VoteRequest) .uverse.error}, allowedDAOs []string) struct{DAO gno.land/r/gov/dao.DAO; AllowedDAOs []string}
NewVoteRequest(option string, proposalID int64) struct{Option gno.land/r/gov/dao.VoteOption; ProposalID gno.land/r/gov/dao.ProposalID; Metadata interface {}}
NewVoteRequestWithMetadata(option string, proposalID int64, metadata interface {}) struct{Option gno.land/r/gov/dao.VoteOption; ProposalID gno.land/r/gov/dao.ProposalID; Metadata interface {}}
Render(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}, p string) string
UpdateImpl(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}, r struct{DAO gno.land/r/gov/dao.DAO; AllowedDAOs []string})
VoteOnProposal(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}, r struct{Option gno.land/r/gov/dao.VoteOption; ProposalID gno.land/r/gov/dao.ProposalID; Metadata interface {}}) interface {Error func() string}
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.