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

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}