Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}