Skip to content

Commit

Permalink
feat(PoC,gov): initial sys/validators and gov/dao contracts (#1945)
Browse files Browse the repository at this point in the history
This PR showcases a model that combines multiple contracts with defined
goals and constraints. The aim is to make everything in `sys/*` usable
by the chain (tm2 powered) efficiently, with minimal need for updates
while maintaining flexibility in usage.

The `sys/` contracts focus on defining data types and helpers to ensure
that received callbacks meet minimal constraints, like GovDAO approval.
They do not handle DAO logic or state due to complexity and
upgradability requirements for DAOs.

I won't include complete DAO examples in this PR. Imagine having these
sections once everything is done:
-  `{p,r}/sys: minimal interface with the chain`
-  `{p,r}/gov: simple DAO frameworks`
- `{p,r}/*`: where users will develop permissionless logic and propose
it to `gov` for approval using `sys` to trigger the chain.

Personal note -> try to introduce and document the notion of "pausable
threads". Related with #1974.

---

TODO:
- [x] pseudo-code for proof of conribution's valset management.
- [x] proposal example.
- [ ] pseudo-code for gnosdk v0 to catch the event and apply the change
from tm2. cc @gfanton
- [ ] add unit-tests, to illustrate the expected usage.

depends on std.Emit (#575).
depends on #1948 (need rebase).

---------

Signed-off-by: moul <94029+moul@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Co-authored-by: Milos Zivkovic <milos.zivkovic@tendermint.com>
Co-authored-by: gfanton <8671905+gfanton@users.noreply.github.com>
  • Loading branch information
3 people authored Jun 12, 2024
1 parent e3f33a2 commit 145f612
Show file tree
Hide file tree
Showing 17 changed files with 346 additions and 63 deletions.
53 changes: 26 additions & 27 deletions contribs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ help:
@echo "Available make commands:"
@cat Makefile | grep '^[a-z][^:]*:' | cut -d: -f1 | sort | sed 's/^/ /'

programs=gnodev gnofaucet

# command to run dependency utilities, like goimports.
rundep=go run -modfile ../misc/devdeps/go.mod

Expand All @@ -25,15 +27,31 @@ GOTEST_FLAGS ?= -v -p 1 -timeout=30m
########################################
# Dev tools
.PHONY: install
install: install.gnomd install.gnodev install.gnofaucet
install:
@echo 'To install a tool, go to the subdirectory, then run `make install`.'
@echo 'To do a full installation, run `make install_all`.'

install_all: $(addsuffix .install,$(programs))
%.install:
@echo "[+] make -C $(basename $@) install"
$(MAKE) --no-print-directory -C $(basename $@) install
.PHONY: install_all

install.gnomd:; cd gnomd && go install .
install.gnodev:; $(MAKE) -C ./gnodev install
install.gnofaucet:; $(MAKE) -C ./gnofaucet install
########################################
# Test suite
test: $(addsuffix .test,$(programs))
%.test:
@echo "[+] make -C $(basename $@) install"
$(MAKE) --no-print-directory -C $(basename $@) test
.PHONY: test

.PHONY: clean
clean:
rm -rf build
########################################
# Lint
.PHONY: lint
lint: $(addsuffix .lint,$(programs))
%.lint:
@echo "[+] make -C $(basename $@) install"
$(MAKE) --no-print-directory -C $(basename $@) lint

########################################
# Dev tools
Expand All @@ -45,23 +63,4 @@ fmt:

.PHONY: tidy
tidy:
@for gomod in `find . -name go.mod`; do ( \
dir=`dirname $$gomod`; \
set -xe; \
cd $$dir; \
go mod tidy -v; \
); done

########################################
# Test suite
.PHONY: test
test: test.gnodev
test.gnodev:
$(MAKE) -C ./gnodev test

########################################
# Lint
.PHONY: test
lint: lint.gnodev
lint.gnodev:
$(MAKE) -C ./gnodev test
find . -name go.mod -execdir go mod tidy -v \;
1 change: 1 addition & 0 deletions examples/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ OFFICIAL_PACKAGES = ./gno.land/p
OFFICIAL_PACKAGES += ./gno.land/r/demo
OFFICIAL_PACKAGES += ./gno.land/r/gnoland
OFFICIAL_PACKAGES += ./gno.land/r/sys
OFFICIAL_PACKAGES += ./gno.land/r/gov

########################################
# Dev tools
Expand Down
20 changes: 15 additions & 5 deletions examples/gno.land/p/demo/ownable/ownable.gno
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ type Ownable struct {

func New() *Ownable {
return &Ownable{
owner: std.GetOrigCaller(),
owner: std.PrevRealm().Addr(),
}
}

func NewWithAddress(addr std.Address) *Ownable {
return &Ownable{owner: addr}
}

// TransferOwnership transfers ownership of the Ownable struct to a new address
func (o *Ownable) TransferOwnership(newOwner std.Address) error {
err := o.CallerIsOwner()
Expand Down Expand Up @@ -44,14 +48,20 @@ func (o *Ownable) DropOwnership() error {
return nil
}

func (o Ownable) Owner() std.Address {
return o.owner
}

// CallerIsOwner checks if the caller of the function is the Realm's owner
func (o *Ownable) CallerIsOwner() error {
if std.GetOrigCaller() == o.owner {
func (o Ownable) CallerIsOwner() error {
if std.PrevRealm().Addr() == o.owner {
return nil
}
return ErrUnauthorized
}

func (o *Ownable) Owner() std.Address {
return o.owner
func (o Ownable) AssertCallerIsOwner() {
if std.PrevRealm().Addr() != o.owner {
panic(ErrUnauthorized)
}
}
58 changes: 36 additions & 22 deletions examples/gno.land/p/demo/ownable/ownable_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -11,57 +11,69 @@ var (
)

func TestNew(t *testing.T) {
std.TestSetOrigCaller(firstCaller)
std.TestSetRealm(std.NewUserRealm(firstCaller))
std.TestSetOrigCaller(firstCaller) // TODO(bug): should not be needed

result := New()
if firstCaller != result.owner {
t.Fatalf("Expected %s, got: %s\n", firstCaller, result.owner)
o := New()
got := o.Owner()
if firstCaller != got {
t.Fatalf("Expected %s, got: %s", firstCaller, got)
}
}

func TestOwner(t *testing.T) {
std.TestSetOrigCaller(firstCaller)
func TestNewWithAddress(t *testing.T) {
o := NewWithAddress(firstCaller)

got := o.Owner()
if firstCaller != got {
t.Fatalf("Expected %s, got: %s", firstCaller, got)
}
}

result := New()
resultOwner := result.Owner()
func TestOwner(t *testing.T) {
std.TestSetRealm(std.NewUserRealm(firstCaller))

o := New()
expected := firstCaller
if resultOwner != expected {
t.Fatalf("Expected %s, got: %s\n", expected, result)
got := o.Owner()
if expected != got {
t.Fatalf("Expected %s, got: %s", expected, got)
}
}

func TestTransferOwnership(t *testing.T) {
std.TestSetOrigCaller(firstCaller)
std.TestSetRealm(std.NewUserRealm(firstCaller))

o := New()

err := o.TransferOwnership(secondCaller)
if err != nil {
t.Fatalf("TransferOwnership failed, %v", err)
}

result := o.Owner()
if secondCaller != result {
t.Fatalf("Expected: %s, got: %s\n", secondCaller, result)
got := o.Owner()
if secondCaller != got {
t.Fatalf("Expected: %s, got: %s", secondCaller, got)
}
}

func TestCallerIsOwner(t *testing.T) {
std.TestSetOrigCaller(firstCaller)
std.TestSetRealm(std.NewUserRealm(firstCaller))

o := New()
unauthorizedCaller := secondCaller

std.TestSetOrigCaller(unauthorizedCaller)
std.TestSetRealm(std.NewUserRealm(unauthorizedCaller))
std.TestSetOrigCaller(unauthorizedCaller) // TODO(bug): should not be needed

err := o.CallerIsOwner()
if err == nil {
t.Fatalf("Expected %s to not be owner\n", unauthorizedCaller)
t.Fatalf("Expected %s to not be owner", unauthorizedCaller)
}
}

func TestDropOwnership(t *testing.T) {
std.TestSetOrigCaller(firstCaller)
std.TestSetRealm(std.NewUserRealm(firstCaller))

o := New()

Expand All @@ -72,18 +84,20 @@ func TestDropOwnership(t *testing.T) {

owner := o.Owner()
if owner != "" {
t.Fatalf("Expected owner to be empty, not %s\n", owner)
t.Fatalf("Expected owner to be empty, not %s", owner)
}
}

// Errors

func TestErrUnauthorized(t *testing.T) {
std.TestSetOrigCaller(firstCaller)
std.TestSetRealm(std.NewUserRealm(firstCaller))
std.TestSetOrigCaller(firstCaller) // TODO(bug): should not be needed

o := New()

std.TestSetOrigCaller(secondCaller)
std.TestSetRealm(std.NewUserRealm(secondCaller))
std.TestSetOrigCaller(secondCaller) // TODO(bug): should not be needed

err := o.TransferOwnership(firstCaller)
if err != ErrUnauthorized {
Expand All @@ -97,7 +111,7 @@ func TestErrUnauthorized(t *testing.T) {
}

func TestErrInvalidAddress(t *testing.T) {
std.TestSetOrigCaller(firstCaller)
std.TestSetRealm(std.NewUserRealm(firstCaller))

o := New()

Expand Down
1 change: 1 addition & 0 deletions examples/gno.land/p/gov/proposal/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module proposal
39 changes: 39 additions & 0 deletions examples/gno.land/p/gov/proposal/proposal.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Package proposal provides a structure for executing proposals.
package proposal

// NewExecutor creates a new executor with the provided callback function.
func NewExecutor(callback func() error) Executor {
return &executorImpl{
callback: callback,
done: false,
}
}

// executorImpl is an implementation of the Executor interface.
type executorImpl struct {
callback func() error
done bool
success bool
}

// execute runs the executor's callback function.
func (exec *executorImpl) Execute() error {
if exec.done {
return ErrAlreadyDone
}
// XXX: assertCalledByGovdao
err := exec.callback()
exec.done = true
exec.success = err == nil
return err
}

// Done returns whether the executor has been executed.
func (exec *executorImpl) Done() bool {
return exec.done
}

// Success returns whether the execution was successful.
func (exec *executorImpl) Success() bool {
return exec.success
}
16 changes: 16 additions & 0 deletions examples/gno.land/p/gov/proposal/types.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Package proposal defines types for proposal execution.
package proposal

import "errors"

// Executor represents a minimal closure-oriented proposal design.
// It is intended to be used by a govdao governance proposal (v1, v2, etc).
type Executor interface {
Execute() error
Done() bool
Success() bool // Done() && !err
}

// ErrAlreadyDone is the error returned when trying to execute an already
// executed proposal.
var ErrAlreadyDone = errors.New("already executed")
6 changes: 6 additions & 0 deletions examples/gno.land/r/gnoland/valopers/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module valopers

require (
gno.land/p/demo/ownable v0.0.0-latest
gno.land/r/gov/dao v0.0.0-latest
)
39 changes: 39 additions & 0 deletions examples/gno.land/r/gnoland/valopers/valopers.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Package valopers is designed around the permissionless lifecycle of valoper profiles.
// It also includes parts designed for govdao to propose valset changes based on registered valopers.
package valopers

import (
"std"

"gno.land/p/demo/ownable"
govdao "gno.land/r/gov/dao"
)

// Valoper represents a validator operator profile.
type Valoper struct {
ownable.Ownable // Embedding the Ownable type for ownership management.

DisplayName string // The display name of the valoper.
ValidatorAddr std.Address // The address of the validator.
// TODO: Add other valoper metadata as needed.
}

// Register registers a new valoper.
// TODO: Define the parameters and implement the function.
func Register( /* TBD */ ) {
panic("not implemented")
}

// Update updates an existing valoper.
// TODO: Define the parameters and implement the function.
func Update( /* TBD */ ) {
panic("not implemented")
}

// GovXXX is a placeholder for a function to interact with the governance DAO.
// TODO: Define a good API and implement it.
func GovXXX() {
// Assert that the caller is a member of the governance DAO.
govdao.AssertIsMember(std.PrevRealm().Addr())
panic("not implemented")
}
Loading

0 comments on commit 145f612

Please sign in to comment.