Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: govdao mvp improvement #2344

Merged
merged 8 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion examples/gno.land/p/gov/proposal/proposal.gno
Original file line number Diff line number Diff line change
@@ -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?
moul marked this conversation as resolved.
Show resolved Hide resolved

// NewExecutor creates a new executor with the provided callback function.
func NewExecutor(callback func() error) Executor {
return &executorImpl{
Expand All @@ -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
Expand All @@ -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")
}
}
12 changes: 11 additions & 1 deletion examples/gno.land/p/gov/proposal/types.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
124 changes: 98 additions & 26 deletions examples/gno.land/r/gov/dao/dao.gno
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,136 @@ 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) {
if !IsMember(addr) {
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")
}
}
5 changes: 4 additions & 1 deletion examples/gno.land/r/gov/dao/gno.mod
Original file line number Diff line number Diff line change
@@ -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
)
10 changes: 10 additions & 0 deletions examples/gno.land/r/gov/dao/types.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package govdao

// Status enum.
type Status string

var (
Accepted Status = "accepted"
Active Status = "active"
// Timeout, NotAccepted
)
3 changes: 3 additions & 0 deletions examples/gno.land/r/gov/integration/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Draft

module integration
4 changes: 4 additions & 0 deletions examples/gno.land/r/gov/integration/integration.gno
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions examples/gno.land/r/gov/integration/z_filetest.gno
Original file line number Diff line number Diff line change
@@ -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
9 changes: 3 additions & 6 deletions examples/gno.land/r/gov/proposals/prop1/prop1.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
4 changes: 2 additions & 2 deletions examples/gno.land/r/sys/validators/validators.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
Loading