diff --git a/examples/gno.land/p/gov/proposal/proposal.gno b/examples/gno.land/p/gov/proposal/proposal.gno index f01d3661a5e..d4f151f935a 100644 --- a/examples/gno.land/p/gov/proposal/proposal.gno +++ b/examples/gno.land/p/gov/proposal/proposal.gno @@ -1,6 +1,10 @@ // Package proposal provides a structure for executing proposals. package proposal +import "std" + +const daoPkgPath = "gno.land/r/gov/dao" // XXX: make it configurable with r/sys/vars? + // NewExecutor creates a new executor with the provided callback function. func NewExecutor(callback func() error) Executor { return &executorImpl{ @@ -21,7 +25,7 @@ func (exec *executorImpl) Execute() error { if exec.done { return ErrAlreadyDone } - // XXX: assertCalledByGovdao + assertCalledByGovdao() err := exec.callback() exec.done = true exec.success = err == nil @@ -37,3 +41,21 @@ func (exec *executorImpl) Done() bool { func (exec *executorImpl) Success() bool { return exec.success } + +func (exec executorImpl) Status() Status { + switch { + case exec.success: + return Success + case exec.done: + return Failed + default: + return NotExecuted + } +} + +func assertCalledByGovdao() { + caller := std.CurrentRealm().PkgPath() + if caller != daoPkgPath { + panic("only gov/dao can execute proposals") + } +} diff --git a/examples/gno.land/p/gov/proposal/types.gno b/examples/gno.land/p/gov/proposal/types.gno index a2ebad6f585..d84d1bfd21f 100644 --- a/examples/gno.land/p/gov/proposal/types.gno +++ b/examples/gno.land/p/gov/proposal/types.gno @@ -8,9 +8,19 @@ import "errors" type Executor interface { Execute() error Done() bool - Success() bool // Done() && !err + Success() bool // Done() && !err + Status() Status // human-readable execution status } // ErrAlreadyDone is the error returned when trying to execute an already // executed proposal. var ErrAlreadyDone = errors.New("already executed") + +// Status enum. +type Status string + +var ( + NotExecuted Status = "not_executed" + Success Status = "success" + Failed Status = "failed" +) diff --git a/examples/gno.land/r/gov/dao/dao.gno b/examples/gno.land/r/gov/dao/dao.gno index 37caca9fa19..68f188eb0a4 100644 --- a/examples/gno.land/r/gov/dao/dao.gno +++ b/examples/gno.land/r/gov/dao/dao.gno @@ -2,60 +2,88 @@ package govdao import ( "std" + "strconv" - "gno.land/p/gov/proposal" + "gno.land/p/demo/ufmt" + pproposal "gno.land/p/gov/proposal" ) -var proposals = make([]Proposal, 0) +var proposals = make([]*proposal, 0) // XXX var members ... -// Proposal represents a proposal in the governance system. -type Proposal struct { - author std.Address +type proposal struct { idx int - Comment string - Executor proposal.Executor + author std.Address + comment string + executor pproposal.Executor + // XXX: make "accepted" and "finished" managed by an interface that can have various voting implementations + accepted bool + finished bool +} + +func (p proposal) Status() Status { + if p.executor.Done() { + return Status(p.executor.Status()) + } + if p.accepted { + return Accepted + } + // XXX: timeout + // XXX: not_accepted + return Active } // Propose is designed to be called by another contract or with // `maketx run`, not by a `maketx call`. -func Propose(proposal Proposal) int { +func Propose(comment string, executor pproposal.Executor) int { // XXX: require payment? - // XXX: sanitize proposal + if executor == nil { + panic("missing proposal executor") + } caller := std.PrevRealm().Addr() AssertIsMember(caller) - proposal.author = caller - proposal.idx = len(proposals) - proposals = append(proposals, proposal) - return proposal.idx + + prop := &proposal{ + comment: comment, + executor: executor, + author: caller, + idx: len(proposals), + } + + proposals = append(proposals, prop) + return prop.idx } func VoteOnProposal(idx int, option string) { + assertProposalExists(idx) caller := std.PrevRealm().Addr() AssertIsMember(caller) - panic("not implemented") - // XXX: implement the voting (woudl be cool to have a generic p/) + + prop := getProposal(idx) + if prop.finished { + panic("prop is not active anymore, cannot vote.") + } + // XXX: implement the real voting (would be cool to have a generic p/) + prop.accepted = option == "YES" + prop.finished = true } func ExecuteProposal(idx int) { assertProposalExists(idx) - // XXX: assert voting is finished - // XXX: assert voting result is YES - // XXX: proposal was not already executed - proposal := proposals[idx] - proposal.Executor.Execute() -} - -func assertProposalExists(idx int) { - if idx < 0 || idx >= len(proposals) { - panic("invalid proposal id") + prop := getProposal(idx) + if !prop.finished { + panic("prop is still active, cannot execute.") } + if !prop.accepted { + panic("prop is not accepted, cannot execute.") + } + prop.executor.Execute() } func IsMember(addr std.Address) bool { // XXX: implement - return true + return true // in the meantime, everyone is a DAO member } func AssertIsMember(addr std.Address) { @@ -63,3 +91,47 @@ func AssertIsMember(addr std.Address) { panic("caller is not member of govdao") } } + +func Render(path string) string { + if path == "" { + output := "" + for idx, prop := range proposals { + output += ufmt.Sprintf("- [/r/gov/dao:%d](%d) - %s (by %s)", idx, idx, prop.comment, prop.author) + } + return output + } + + // else display the proposal + idx, err := strconv.Atoi(path) + if err != nil { + return "404" + } + + if !proposalExists(idx) { + return "404" + } + prop := getProposal(idx) + output := "" + output += ufmt.Sprintf("# Prop#%d", prop.idx) + "\n" + output += "\n" + output += prop.comment + output += "\n" + output += ufmt.Sprintf("Status: %s", string(prop.Status())) + output += "\n" + output += ufmt.Sprintf("Author: %s", string(prop.author)) + return output +} + +func getProposal(idx int) *proposal { + return proposals[idx-1] +} + +func proposalExists(idx int) bool { + return idx > 0 && idx <= len(proposals) +} + +func assertProposalExists(idx int) { + if !proposalExists(idx) { + panic("invalid proposal id") + } +} diff --git a/examples/gno.land/r/gov/dao/gno.mod b/examples/gno.land/r/gov/dao/gno.mod index ae296cf121c..0127e84a8e2 100644 --- a/examples/gno.land/r/gov/dao/gno.mod +++ b/examples/gno.land/r/gov/dao/gno.mod @@ -1,3 +1,6 @@ module gno.land/r/gov/dao -require gno.land/p/gov/proposal v0.0.0-latest +require ( + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/gov/proposal v0.0.0-latest +) diff --git a/examples/gno.land/r/gov/dao/types.gno b/examples/gno.land/r/gov/dao/types.gno new file mode 100644 index 00000000000..9e4ca0718c9 --- /dev/null +++ b/examples/gno.land/r/gov/dao/types.gno @@ -0,0 +1,10 @@ +package govdao + +// Status enum. +type Status string + +var ( + Accepted Status = "accepted" + Active Status = "active" + // Timeout, NotAccepted +) diff --git a/examples/gno.land/r/gov/integration/gno.mod b/examples/gno.land/r/gov/integration/gno.mod new file mode 100644 index 00000000000..f584f375133 --- /dev/null +++ b/examples/gno.land/r/gov/integration/gno.mod @@ -0,0 +1,3 @@ +// Draft + +module integration diff --git a/examples/gno.land/r/gov/integration/integration.gno b/examples/gno.land/r/gov/integration/integration.gno new file mode 100644 index 00000000000..5b47b3dec83 --- /dev/null +++ b/examples/gno.land/r/gov/integration/integration.gno @@ -0,0 +1,4 @@ +// Package integration tests the govdao ecosystem from an external perspective. +// It aims to confirm that the system can remain static while supporting +// additional DAO use cases over time. +package integration diff --git a/examples/gno.land/r/gov/integration/z_filetest.gno b/examples/gno.land/r/gov/integration/z_filetest.gno new file mode 100644 index 00000000000..a85588e4f11 --- /dev/null +++ b/examples/gno.land/r/gov/integration/z_filetest.gno @@ -0,0 +1,45 @@ +package main + +import ( + govdao "gno.land/r/gov/dao" + _ "gno.land/r/gov/proposals/prop1" +) + +func main() { + println("--") + println(govdao.Render("")) + println("--") + println(govdao.Render("1")) + println("--") + govdao.VoteOnProposal(1, "YES") + println("--") + println(govdao.Render("1")) + println("--") + govdao.ExecuteProposal(1) + println("--") + println(govdao.Render("1")) +} + +// Output: +// -- +// - [/r/gov/dao:0](0) - manual valset changes proposal example (by g1tk0fscr3p5g8hnhxq6v93jxcpm5g3cstlxfxa3) +// -- +// # Prop#0 +// +// manual valset changes proposal example +// Status: active +// Author: g1tk0fscr3p5g8hnhxq6v93jxcpm5g3cstlxfxa3 +// -- +// -- +// # Prop#0 +// +// manual valset changes proposal example +// Status: accepted +// Author: g1tk0fscr3p5g8hnhxq6v93jxcpm5g3cstlxfxa3 +// -- +// -- +// # Prop#0 +// +// manual valset changes proposal example +// Status: success +// Author: g1tk0fscr3p5g8hnhxq6v93jxcpm5g3cstlxfxa3 diff --git a/examples/gno.land/r/gov/proposals/prop1/prop1.gno b/examples/gno.land/r/gov/proposals/prop1/prop1.gno index 6e5edb0c97b..68cd9d60bfa 100644 --- a/examples/gno.land/r/gov/proposals/prop1/prop1.gno +++ b/examples/gno.land/r/gov/proposals/prop1/prop1.gno @@ -27,13 +27,10 @@ func init() { // Wraps changesFn to emit a certified event only if executed from a // complete governance proposal process. - executor := validators.NewProposalExecutor(changesFn) + executor := validators.NewPropExecutor(changesFn) // Create a proposal. // XXX: payment - proposal := govdao.Proposal{ - Comment: "manual valset changes proposal example", - Executor: executor, - } - govdao.Propose(proposal) + comment := "manual valset changes proposal example" + govdao.Propose(comment, executor) } diff --git a/examples/gno.land/r/sys/validators/validators.gno b/examples/gno.land/r/sys/validators/validators.gno index 0e6e132f6e9..5fb08ebbfc7 100644 --- a/examples/gno.land/r/sys/validators/validators.gno +++ b/examples/gno.land/r/sys/validators/validators.gno @@ -15,10 +15,10 @@ type Change struct { Power int } -// NewProposalExecutor creates a new executor that wraps a changes closure +// NewPropExecutor creates a new executor that wraps a changes closure // proposal. It emits a typed object (subscribed by tm2) only if it passes // through a complete p/gov/proposal process. -func NewProposalExecutor(changesFn func() []Change) proposal.Executor { +func NewPropExecutor(changesFn func() []Change) proposal.Executor { if changesFn == nil { panic("changesFn should not be nil") }