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

proxy.gno

9.18 Kb · 269 lines
  1package dao
  2
  3import (
  4	"chain"
  5	"errors"
  6	"strconv"
  7	"strings"
  8
  9	"gno.land/p/nt/ufmt/v0"
 10)
 11
 12// dao is the actual govDAO implementation, having all the needed business logic
 13var dao DAO
 14
 15// allowedDAOs contains realms that can be used to update the actual govDAO implementation,
 16// and validate Proposals.
 17// This is like that to be able to rollback using a previous govDAO implementation in case
 18// the latest implementation has a breaking bug. After a test period, a proposal can be
 19// executed to remove all previous govDAOs implementations and leave the last one.
 20var allowedDAOs []string
 21
 22// proposals contains all the proposals in history.
 23var proposals *Proposals = NewProposals()
 24
 25// Render calls directly to Render's DAO implementation.
 26// This allows to have this realm as the main entry point for everything.
 27func Render(cur realm, p string) string {
 28	if dao == nil {
 29		return "DAO not initialized"
 30	}
 31	return dao.Render(cross(cur), cur.PkgPath(), p)
 32}
 33
 34// MustCreateProposal is an utility method that does the same as CreateProposal,
 35// but instead of erroing if something happens, it panics.
 36func MustCreateProposal(cur realm, r ProposalRequest) ProposalID {
 37	pid, err := CreateProposal(cur, r)
 38	if err != nil {
 39		panic(err.Error())
 40	}
 41
 42	return pid
 43}
 44
 45// ExecuteProposal will try to execute the proposal with the provided ProposalID.
 46// If the proposal was denied, it will return false. If the proposal is correctly
 47// executed, it will return true. If something happens this function will panic.
 48func ExecuteProposal(cur realm, pid ProposalID) bool {
 49	return executeProposal(cur, pid, false)
 50}
 51
 52// ExecuteOrRejectProposal executes the proposal with the provided ProposalID or rejects
 53// it when there is an execution error.
 54// If the proposal was denied, it will return false. If the proposal is correctly
 55// executed, it will return true, unless execution fails with an error, in which case
 56// proposal is rejected with the error as the reason.
 57// This function allows to finish proposals by rejecting them when there is a state
 58// change or an error in the proposal parameters that makes execution fail, potentially
 59// leaving the proposal active forever because it can't be successfully executed.
 60func ExecuteOrRejectProposal(cur realm, pid ProposalID) bool {
 61	return executeProposal(cur, pid, true)
 62}
 63
 64// CreateProposal will try to create a new proposal, that will be validated by the actual
 65// govDAO implementation. If the proposal cannot be created, an error will be returned.
 66func CreateProposal(cur realm, r ProposalRequest) (ProposalID, error) {
 67	if dao == nil {
 68		return -1, errors.New("DAO not initialized")
 69	}
 70	author, err := dao.PreCreateProposal(0, cur, r)
 71	if err != nil {
 72		return -1, err
 73	}
 74
 75	p := &Proposal{
 76		author:      author,
 77		title:       r.title,
 78		description: r.description,
 79		executor:    r.executor,
 80		allowedDAOs: allowedDAOs[:],
 81	}
 82
 83	pid := proposals.SetProposal(p)
 84	dao.PostCreateProposal(0, cur, r, pid)
 85
 86	chain.Emit("ProposalCreated",
 87		"id", strconv.FormatInt(int64(pid), 10),
 88	)
 89
 90	return pid, nil
 91}
 92
 93func MustVoteOnProposal(cur realm, r VoteRequest) {
 94	if err := VoteOnProposal(cur, r); err != nil {
 95		panic(err.Error())
 96	}
 97}
 98
 99// VoteOnProposal sends a vote to the actual govDAO implementation.
100// If the voter cannot vote the specified proposal, this method will return an error
101// with the explanation of why.
102func VoteOnProposal(cur realm, r VoteRequest) error {
103	if dao == nil {
104		return errors.New("DAO not initialized")
105	}
106	return dao.VoteOnProposal(0, cur, r)
107}
108
109// MustVoteOnProposalSimple is like MustVoteOnProposal but intended to be used through gnokey with basic types.
110func MustVoteOnProposalSimple(cur realm, pid int64, option string) {
111	MustVoteOnProposal(cur, VoteRequest{
112		Option:     VoteOption(option),
113		ProposalID: ProposalID(pid),
114	})
115}
116
117func MustGetProposal(pid ProposalID) *Proposal {
118	p, err := GetProposal(pid)
119	if err != nil {
120		panic(err.Error())
121	}
122
123	return p
124}
125
126// GetProposal gets created proposal by its ID. Non-crossing pure read:
127// looks up the proposal in this realm's package var. Callable directly
128// from any realm without cross-call syntax.
129func GetProposal(pid ProposalID) (*Proposal, error) {
130	if dao == nil {
131		return nil, errors.New("DAO not initialized")
132	}
133	prop := proposals.GetProposal(pid)
134	if prop == nil {
135		return nil, errors.New(ufmt.Sprintf("Proposal %v does not exist.", int64(pid)))
136	}
137	return prop, nil
138}
139
140// UpdateImpl is a method intended to be used on a proposal.
141// This method will update the current govDAO implementation
142// to a new one. AllowedDAOs are a list of realms that can
143// call this method, in case the new DAO implementation had
144// a breaking bug. A nil DAO is ignored.
145// If AllowedDAOs field is not set correctly, the actual DAO
146// implementation wont be able to execute new Proposals!
147//
148// An empty AllowedDAOs is ignored rather than stored. An empty list makes
149// InAllowedDAOs() return true for every caller — the bootstrap-only state that
150// lets the genesis MsgRun seed the member set. Since this is the only site that
151// assigns allowedDAOs, ignoring empty here makes the transition
152// empty -> non-empty one-way: once locked down the DAO cannot be reopened,
153// whether the empty value arrives as a literal []string{} or from
154// NewUpdateRequest(d, nil), which copies nil into a non-nil empty slice.
155// Individual entries must be non-blank realm paths; an empty entry would match
156// a user realm's empty PkgPath() and is rejected.
157func UpdateImpl(cur realm, r UpdateRequest) {
158	// AGENTS.md: in a crossing function, always check IsCurrent() before
159	// deriving caller identity from cur.Previous(). Redundant under the
160	// crossing-frame guarantee, but mandated, and this is the single most
161	// powerful entrypoint (it rewrites the allowlist and swaps the impl).
162	if !cur.IsCurrent() {
163		panic("UpdateImpl: realm value is not the caller's live cur")
164	}
165	gRealm := cur.Previous().PkgPath()
166
167	if !InAllowedDAOs(gRealm) {
168		panic("permission denied for prev realm: " + gRealm)
169	}
170
171	if len(r.AllowedDAOs) != 0 {
172		// Every entry must be a real realm path. len() != 0 alone is the wrong
173		// invariant: InAllowedDAOs compares by exact string, and a user realm's
174		// PkgPath() is "", so a single "" entry admits any caller whose previous
175		// frame is a user realm -- the same fail-open outcome this guard exists
176		// to prevent, just spelled differently. An empty entry can only be a
177		// drafting mistake, so reject the whole request rather than silently
178		// dropping it and storing a list the proposal did not describe.
179		for i, d := range r.AllowedDAOs {
180			trimmed := strings.TrimSpace(d)
181			if trimmed == "" {
182				panic("AllowedDAOs entries must be realm paths; got an empty one")
183			}
184			// Entries are stored exactly as given, and InAllowedDAOs compares
185			// whole strings, so an entry with surrounding spaces matches no
186			// caller at all. A non-empty list also closes the bootstrap
187			// window, so a list of only padded entries is locked shut against
188			// everyone, the DAO included, with no way to reopen it.
189			//
190			// This does not make the list typo-proof, and is not meant to be:
191			// any wrong path locks the DAO out exactly the same way, and no
192			// check here can tell a typo from a realm that does not exist yet.
193			// Whitespace is worth rejecting because it is the one spelling a
194			// human reviewing the proposal cannot see. Rejected rather than
195			// trimmed, so what gets stored is what the proposal said.
196			// Reported by position, not by value. A panic message becomes the
197			// proposal's DeniedReason, which is stored, and the entry is
198			// caller-supplied and unbounded — echoing it would put an
199			// arbitrary amount of someone else's text into this realm's
200			// storage. The index is enough to find it in a list the proposal
201			// author wrote.
202			if d != trimmed {
203				panic("AllowedDAOs entries must not have leading or trailing spaces; entry " + strconv.Itoa(i))
204			}
205		}
206		// Stored as given. A defensive copy here looks prudent but would
207		// guard a write no outside realm can perform, which was checked from
208		// a separate realm rather than assumed:
209		//
210		//   - Building this request as a literal fails outright, with
211		//     "cannot allocate gno.land/r/gov/dao.UpdateRequest in realm ...".
212		//   - Writing through a request obtained from NewUpdateRequest fails
213		//     with "cannot directly modify readonly tainted object".
214		//
215		// So the only way in is NewUpdateRequest, which copies already. Both
216		// checks are language guarantees, not conventions.
217		allowedDAOs = r.AllowedDAOs
218	}
219
220	if r.DAO != nil {
221		dao = r.DAO
222	}
223}
224
225func AllowedDAOs() []string {
226	dup := make([]string, len(allowedDAOs))
227	copy(dup, allowedDAOs)
228	return dup
229}
230
231func InAllowedDAOs(pkg string) bool {
232	if len(allowedDAOs) == 0 {
233		return true // corner case for initialization
234	}
235	for _, d := range allowedDAOs {
236		if pkg == d {
237			return true
238		}
239	}
240	return false
241}
242
243func executeProposal(cur realm, pid ProposalID, execErrorRejects bool) bool {
244	if dao == nil {
245		return false
246	}
247	execute, err := dao.PreExecuteProposal(0, cur, pid)
248	if err != nil {
249		panic(err.Error())
250	}
251
252	if !execute {
253		return false
254	}
255	prop, err := GetProposal(pid)
256	if err != nil {
257		panic(err.Error())
258	}
259
260	err = dao.ExecuteProposal(0, cur, pid, prop.executor)
261	if err != nil {
262		if execErrorRejects {
263			return false
264		}
265
266		panic(err.Error())
267	}
268	return true
269}