daokit.gno
6.17 Kb · 154 lines
1package daokit
2
3import (
4 "time"
5
6 "gno.land/p/samcrew/daocond"
7)
8
9// Every entry point threads the DAO realm's own realm value.
10//
11// It serves two purposes. Execute forwards it to the action handlers, which
12// cross out under it, so the implementation must refuse any realm that is not
13// its own — otherwise a caller holding this handle could make the DAO act under
14// the CALLER's identity. And because the realm is then known to be the DAO's,
15// rlm.Previous() names the DAO's immediate caller, which a stack walk cannot do:
16// reached through this handle there is no DAO realm frame on the stack at all,
17// so a walk reports the signing account instead.
18//
19// An implementation must require the realm to be BOTH its own by pkgpath and
20// current: every realm the DAO crosses out into receives the DAO's own realm as
21// its cur.Previous() and can hand it back within the same transaction, and only
22// IsCurrent() tells that apart from the live one.
23//
24// The realm is deliberately never the first parameter: a function whose first
25// parameter is a realm is read as crossing, which /p/ packages may not declare.
26type DAO interface {
27 // Creates a new proposal and returns its ID.
28 Propose(req ProposalRequest, rlm realm) uint64
29 // Casts a vote on a specific proposal.
30 Vote(id uint64, vote daocond.Vote, rlm realm)
31 // Executes a proposal if it meets the required conditions.
32 Execute(id uint64, rlm realm)
33
34 // Generates a web interface representation for the given path.
35 Render(path string) string
36
37 // Gets an extension by path, returns nil if not found. A PRIVATE extension
38 // additionally requires the DAO realm's own live realm, on the same terms
39 // as the entry points above; a public one ignores it.
40 Extension(path string, rlm realm) Extension
41 // Lists all available extensions.
42 ExtensionsList() ExtensionsList
43}
44
45// Function type for updating DAO implementation during governance upgrades.
46type SetImplemFn = func(implem DAO)
47
48// Creates, votes yes, and immediately executes a proposal in one operation.
49// Useful when you have enough permission to execute an action directly.
50// Examples: migrations, adding members.
51//
52// SAME REALM ONLY. Every step threads rlm, and each entry point requires it to
53// be the receiving DAO's own — so d must be a DAO hosted by the realm calling
54// this. Handing it a DAO belonging to another realm is refused.
55//
56// That refusal is the point. Driving a foreign DAO by passing it your realm is
57// the donation shape: it makes the DAO act under YOUR identity, and it is what a
58// caller holding the handle could previously do to any DAO. It appeared to work
59// only because nothing checked.
60//
61// A parent DAO can still drive a child. Reach the child through the child
62// REALM's own crossing function — child.Propose(cross(cur), req) — so the child
63// mints its own realm and sees the parent as its caller. Make the parent a
64// member of the child; the child then authorises it like any other member.
65func InstantExecute(d DAO, req ProposalRequest, rlm realm) uint64 {
66 id := d.Propose(req, rlm)
67 d.Vote(id, daocond.VoteYes, rlm)
68 d.Execute(id, rlm)
69 return id
70}
71
72// Manages the essential components of a DAO: resources, proposals, and extensions.
73type Core struct {
74 Resources *ResourcesStore // Available actions and their conditions
75 Proposals *ProposalsStore // All proposals and their voting state
76 Extensions *ExtensionsStore // Pluggable functionality modules
77}
78
79// Creates a new DAO core with empty stores.
80func NewCore() *Core {
81 return &Core{
82 Resources: NewResourcesStore(),
83 Proposals: NewProposalsStore(),
84 Extensions: &ExtensionsStore{},
85 }
86}
87
88// Records a vote for a specific proposal from a voter.
89func (d *Core) Vote(voterID string, proposalID uint64, vote daocond.Vote) {
90 proposal := d.Proposals.GetProposal(proposalID)
91 if proposal == nil {
92 panic("proposal not found")
93 }
94
95 // Same terminal state as Execute: a ballot stays open right up until the
96 // proposal executes, so support can still be withdrawn after the condition
97 // is first met. Anything narrower would let a proposal be locked in.
98 if proposal.Status == ProposalStatusExecuted {
99 panic("proposal is already executed")
100 }
101
102 proposal.Ballot.Vote(voterID, vote)
103}
104
105// Executes a proposal if it meets the required voting conditions.
106func (d *Core) Execute(proposalID uint64, rlm realm) {
107 proposal := d.Proposals.GetProposal(proposalID)
108 if proposal == nil {
109 panic("proposal not found")
110 }
111
112 // Executed is the only status that disqualifies a proposal. It used to be
113 // "anything but Open", which made the status a second, weaker gate on top of
114 // the condition — and any realm holding the DAO handle could trip it by
115 // rendering, since the read paths wrote Open -> Passed as they walked.
116 // Whether a proposal may execute is decided by its condition, below, and
117 // nothing else.
118 if proposal.Status == ProposalStatusExecuted {
119 panic("proposal is already executed")
120 }
121
122 if !proposal.Condition.Eval(proposal.Ballot) {
123 panic("proposal condition is not met")
124 }
125
126 // Marked BEFORE the handler runs, not after. The handler crosses out of the
127 // DAO, so a callee can re-enter here within the same transaction; marking
128 // afterwards would let it execute the same proposal again. The old code was
129 // protected from this only incidentally, by a "must be Open" test that also
130 // made rendering able to brick a proposal.
131 //
132 // The cost is that Execute is NOT retry-safe. gno's recover() does not roll
133 // back realm state, so a host realm that recovers around a failing Execute
134 // keeps the marker and burns the proposal permanently. Do not recover around
135 // it: let the transaction abort, which reverts the marker with everything
136 // else. Marking afterwards would trade this for re-entrancy, which is worse.
137 proposal.Status = ProposalStatusExecuted
138 proposal.ExecutedAt = time.Now()
139
140 d.Resources.Get(proposal.Action.Type()).Handler.Execute(proposal.Action, rlm)
141}
142
143// Creates a new proposal for voting and returns its ID.
144func (d *Core) Propose(proposerID string, req ProposalRequest) uint64 {
145 actionType := req.Action.Type()
146
147 resource := d.Resources.Get(actionType)
148 if resource == nil {
149 panic("action type is not registered as a resource")
150 }
151
152 prop := d.Proposals.newProposal(proposerID, req, resource.Condition)
153 return uint64(prop.ID)
154}