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.

lifecycle.gno

10.60 Kb · 371 lines
  1package grants
  2
  3// This file holds every state transition of the board. Each one takes the
  4// acting address and the block height from the caller rather than reading
  5// them from the chain, which is what keeps the package pure and the whole
  6// lifecycle exercisable in a plain unit test.
  7
  8// NewMilestone is a small constructor so callers do not build the struct (and
  9// its Attempts slice) by hand.
 10func NewMilestone(title string, amount int64) *Milestone {
 11	return &Milestone{Title: title, Amount: amount}
 12}
 13
 14// Submit files a grant application for the applicant themselves. Anyone may
 15// apply; being a member is not required and does not help, since a member is
 16// barred from voting on their own request.
 17func (b *Board) Submit(applicant address, title, body string, ms []*Milestone, height int64) (*Request, error) {
 18	return b.SubmitFor(applicant, applicant, "", title, body, ms, height)
 19}
 20
 21// SubmitFor files a grant application whose money goes to beneficiary rather
 22// than to the applicant, with a reason for the detour.
 23//
 24// The reason is required, and it is required because it is the only thing on
 25// the page that answers the question a reader will have: why is this person
 26// asking for someone else's money. The usual honest answer is that the payee
 27// has an empty account and cannot pay the gas to ask, which is exactly the
 28// case a grant board exists to serve and exactly the case an impersonation
 29// looks like. Making the claim explicit is what lets a member check it.
 30//
 31// Passing the applicant's own address is the same as Submit and needs no
 32// reason.
 33func (b *Board) SubmitFor(applicant, beneficiary address, reason, title, body string, ms []*Milestone, height int64) (*Request, error) {
 34	if !applicant.IsValid() || !beneficiary.IsValid() {
 35		return nil, ErrBadAddress
 36	}
 37	if len(reason) > MaxReason {
 38		return nil, ErrBadReason
 39	}
 40	if beneficiary != applicant && reason == "" {
 41		return nil, ErrNeedReason
 42	}
 43	if err := checkTitle(title); err != nil {
 44		return nil, err
 45	}
 46	if len(body) > MaxBody {
 47		return nil, ErrBadBody
 48	}
 49	if len(ms) == 0 || len(ms) > MaxMilestones {
 50		return nil, ErrBadMilestones
 51	}
 52	for _, m := range ms {
 53		if m == nil || m.Amount <= 0 {
 54			return nil, ErrBadAmount
 55		}
 56		if err := checkTitle(m.Title); err != nil {
 57			return nil, err
 58		}
 59	}
 60	return b.file(&Request{
 61		Kind:        KindGrant,
 62		Applicant:   applicant,
 63		Beneficiary: beneficiary,
 64		Reason:      reason,
 65		Title:       title,
 66		Body:        body,
 67		Milestones:  ms,
 68		CreatedAt:   height,
 69	}), nil
 70}
 71
 72// SubmitMemberChange files a request to add or remove a board member. Only a
 73// member may file one: opening the board's own composition to anyone with a
 74// keypair is how a grant board gets captured.
 75func (b *Board) SubmitMemberChange(proposer, subject address, add bool, body string, height int64) (*Request, error) {
 76	if !b.IsMember(proposer) {
 77		return nil, ErrNotMember
 78	}
 79	if !subject.IsValid() {
 80		return nil, ErrBadAddress
 81	}
 82	if len(body) > MaxBody {
 83		return nil, ErrBadBody
 84	}
 85	switch {
 86	case add && b.IsMember(subject):
 87		return nil, ErrIsMember
 88	case !add && !b.IsMember(subject):
 89		return nil, ErrNotMember
 90	case !add && b.MemberCount() == 1:
 91		return nil, ErrLastMember
 92	}
 93	title := "Remove " + subject.String()
 94	if add {
 95		title = "Add " + subject.String()
 96	}
 97	return b.file(&Request{
 98		Kind:        KindMember,
 99		Applicant:   proposer,
100		Beneficiary: proposer,
101		Title:       title,
102		Body:        body,
103		Subject:     subject,
104		Add:         add,
105		CreatedAt:   height,
106	}), nil
107}
108
109func (b *Board) file(r *Request) *Request {
110	r.ID = b.nextID
111	b.nextID++
112	b.requests.Set(idKey(r.ID), r)
113	return r
114}
115
116// Withdraw lets an applicant pull their own request before it is decided.
117func (b *Board) Withdraw(caller address, id int, height int64) error {
118	r := b.Get(id)
119	if r == nil {
120		return ErrNoRequest
121	}
122	if r.Applicant != caller {
123		return ErrNotApplicant
124	}
125	if r.Status != Pending {
126		return ErrNotPending
127	}
128	r.Status, r.DecidedAt = Withdrawn, height
129	return nil
130}
131
132// Standing counts only the ballots of addresses that are members right now.
133// Decisions use this, not Request.Tally: a ballot is a permanent record of
134// what someone said, but it stops carrying weight the moment they leave the
135// board. The two numbers differ exactly when a voter has since been removed,
136// and a good renderer shows both.
137func (b *Board) Standing(r *Request) (yes, no int) { return b.standing(r.Votes) }
138
139// Decides previews what a ballot would do without casting it, so a caller can
140// check a precondition it cannot roll back: a realm, for instance, refusing
141// to let a grant carry that its treasury cannot cover. It reports false for a
142// ballot that would be refused anyway.
143func (b *Board) Decides(id int, voter address, approve bool) (bool, Status) {
144	r := b.Get(id)
145	if r == nil || r.Status != Pending {
146		return false, Pending
147	}
148	if !b.IsMember(voter) || r.Excludes(voter) || r.BallotOf(voter) != nil {
149		return false, Pending
150	}
151	yes, no := b.Standing(r)
152	if approve {
153		yes++
154	} else {
155		no++
156	}
157	switch m := b.Majority(r); {
158	case yes >= m:
159		return true, Approved
160	case no >= m:
161		return true, Rejected
162	}
163	return false, Pending
164}
165
166// ReviewDecides is Decides for a proof review: it reports whether this
167// verdict would close the attempt under review, and how. A realm uses it to
168// check, before recording anything, that it can actually pay a tranche it is
169// about to release.
170func (b *Board) ReviewDecides(id, idx int, voter address, accept bool) (bool, Outcome) {
171	r := b.Get(id)
172	if r == nil || r.Kind != KindGrant || r.Status != Approved {
173		return false, UnderReview
174	}
175	if idx < 0 || idx >= len(r.Milestones) {
176		return false, UnderReview
177	}
178	a := r.Milestones[idx].Current()
179	if a == nil {
180		return false, UnderReview
181	}
182	if !b.IsMember(voter) || r.Excludes(voter) || find(a.Reviews, voter) != nil {
183		return false, UnderReview
184	}
185	yes, no := b.standing(a.Reviews)
186	if accept {
187		yes++
188	} else {
189		no++
190	}
191	switch m := b.Majority(r); {
192	case yes >= m:
193		return true, Accepted
194	case no >= m:
195		return true, Refused
196	}
197	return false, UnderReview
198}
199
200// standing counts ballots from current members only. See Standing.
201func (b *Board) standing(bs []Ballot) (yes, no int) {
202	for _, v := range bs {
203		if !b.IsMember(v.Voter) {
204			continue
205		}
206		if v.Approve {
207			yes++
208		} else {
209			no++
210		}
211	}
212	return yes, no
213}
214
215// Vote casts one member's ballot on a request and returns the status it left
216// the request in. A member votes once; there is no changing your mind, which
217// is the price of every ballot being a permanent public statement.
218func (b *Board) Vote(voter address, id int, approve bool, reason string, height int64) (Status, error) {
219	r := b.Get(id)
220	if r == nil {
221		return Pending, ErrNoRequest
222	}
223	if !b.IsMember(voter) {
224		return r.Status, ErrNotMember
225	}
226	if r.Status != Pending {
227		return r.Status, ErrNotPending
228	}
229	if r.Excludes(voter) {
230		return r.Status, ErrConflict
231	}
232	if r.BallotOf(voter) != nil {
233		return r.Status, ErrAlreadyVoted
234	}
235	if len(reason) > MaxReason {
236		return r.Status, ErrBadReason
237	}
238
239	r.Votes = append(r.Votes, Ballot{Voter: voter, Approve: approve, Reason: reason, Height: height})
240
241	yes, no := b.Standing(r)
242	switch m := b.Majority(r); {
243	case yes >= m:
244		r.Status, r.DecidedAt = Approved, height
245		if r.Kind == KindMember {
246			b.applyMemberChange(r, height)
247		}
248	case no >= m:
249		r.Status, r.DecidedAt = Rejected, height
250	}
251	return r.Status, nil
252}
253
254// applyMemberChange is the whole execution engine for KindMember: a carried
255// membership request takes effect at once, with nothing to claim afterwards,
256// so it goes straight from Approved to Completed.
257func (b *Board) applyMemberChange(r *Request, height int64) {
258	if r.Add {
259		b.members.Set(r.Subject.String(), height)
260	} else {
261		b.members.Remove(r.Subject.String())
262	}
263	r.Status = Completed
264}
265
266// SubmitProof offers evidence for the next unreleased milestone of an
267// approved grant. Milestones are earned in order, and only one proof is under
268// review at a time.
269//
270// Either the applicant or the beneficiary may submit. On a request filed for
271// an empty account, the payee cannot transact until the first tranche lands,
272// so restricting this to the payee would strand the grant it was filed to
273// unstick; restricting it to the filer would leave the payee unable to show
274// their own work once they can.
275func (b *Board) SubmitProof(caller address, id, idx int, p Proof) error {
276	r := b.Get(id)
277	if r == nil {
278		return ErrNoRequest
279	}
280	if r.Kind != KindGrant || r.Status != Approved {
281		return ErrNotApproved
282	}
283	if r.Applicant != caller && r.Payee() != caller {
284		return ErrNotApplicant
285	}
286	if idx < 0 || idx >= len(r.Milestones) {
287		return ErrNoMilestone
288	}
289	if idx != r.Next() {
290		return ErrOutOfOrder
291	}
292	if err := checkProof(p); err != nil {
293		return err
294	}
295	m := r.Milestones[idx]
296	if m.Current() != nil {
297		return ErrProofPending
298	}
299	m.Attempts = append(m.Attempts, &Attempt{Proof: p, Outcome: UnderReview})
300	return nil
301}
302
303// Review casts a member's verdict on the proof currently under review and
304// returns the outcome that verdict left the attempt in. Accepting it releases
305// the tranche, which in this package means marking it Released and nothing
306// more; moving the coins is the caller's job, and Request.Paid tells it how
307// much it now owes. Refusing closes the attempt and lets the applicant try
308// again with better evidence.
309func (b *Board) Review(voter address, id, idx int, accept bool, reason string, height int64) (Outcome, error) {
310	r := b.Get(id)
311	if r == nil {
312		return UnderReview, ErrNoRequest
313	}
314	if !b.IsMember(voter) {
315		return UnderReview, ErrNotMember
316	}
317	if r.Kind != KindGrant || r.Status != Approved {
318		return UnderReview, ErrNotApproved
319	}
320	if idx < 0 || idx >= len(r.Milestones) {
321		return UnderReview, ErrNoMilestone
322	}
323	if r.Excludes(voter) {
324		return UnderReview, ErrConflict
325	}
326	if len(reason) > MaxReason {
327		return UnderReview, ErrBadReason
328	}
329	m := r.Milestones[idx]
330	a := m.Current()
331	if a == nil {
332		return UnderReview, ErrNoProof
333	}
334	if find(a.Reviews, voter) != nil {
335		return UnderReview, ErrAlreadyVoted
336	}
337
338	a.Reviews = append(a.Reviews, Ballot{Voter: voter, Approve: accept, Reason: reason, Height: height})
339
340	yes, no := b.standing(a.Reviews)
341	switch mj := b.Majority(r); {
342	case yes >= mj:
343		a.Outcome, a.ClosedAt = Accepted, height
344		m.Released, m.ReleasedAt = true, height
345		if r.Next() == -1 {
346			r.Status = Completed
347		}
348	case no >= mj:
349		a.Outcome, a.ClosedAt = Refused, height
350	}
351	return a.Outcome, nil
352}
353
354func checkTitle(s string) error {
355	if len(s) == 0 || len(s) > MaxTitle {
356		return ErrBadTitle
357	}
358	return nil
359}
360
361func checkProof(p Proof) error {
362	switch p.Kind {
363	case "url", "hash", "text":
364	default:
365		return ErrBadProof
366	}
367	if len(p.Ref) == 0 || len(p.Ref) > MaxRef || len(p.Note) > MaxNote {
368		return ErrBadProof
369	}
370	return nil
371}