I'd intended to put a few more simple toys under my belt before attempting to refine anything related to governance, but instead... I've decided to dive into the deep end and start rolling the packages/realms I'll need to realize the most important element of Commonwealth: The Commune of Communes.
This won't end up any less a toy than anything else I was considering, but will at least give me something to keep expanding on as core principles are baked in, with more clearly defined mechanics emerging from the beauty of Go.
The prototype in mind:
will have a
commune
realm that leverages thedao
packagecommunes will support
Join
,Leave
,Vote
andMember
communes will support
ProposeChange
, creatable by the admin, allowing members to cast a balanced ternaryVote
the admin can call
ProposalResolve
at any time, and the votes will be tallied. if < 2/3, final decision is a 0. if quorum wasn't reached (for now 100% of members), the final decision is 0. if 0 votes are cast, the final vote is -1.
There will be no sessions, or attempt at a social proof or reputation system in this build, simply 1 citizen = 1 vote. I'd started to rough out an IdentityRegistry
package and a citizen
realm for it, but have for now commented out the integrations between the commune
/dao
and the half cooked identity
package. Will return to.
The Package
The dao
package will house the bulk of the features described and will contain the following: idao.gno
, dao.gno
, and iproposal.gno
, proposal.gno
.
The Interfaces
idao.gno
:
type IDAO interface {
// Joins the DAO (must be a valid `identity`)
Join(address_ std.Address) bool
// Leaves the DAO (must be a valid `identity` and already a DAO member)
Leave(address_ std.Address) bool
// Returns membership status in the DAO
Membership(address_ std.Address) bool
// Returns count of members
TotalMembers() uint64
// Creates a proposal from the DAO (must be `admin`)
Propose(proposer_ std.Address, value_ uint64) uint64
// Fetchs the Proposal at a given `idx_`
ProposalAtIndex(idx_ uint64) *Proposal
// Fetchs the most recent Proposal
CurrentProposal() *Proposal
// Resolves a proposal and returns final vote (must be `admin`)
ProposalResolve(caller_ std.Address, propId_ uint64) int8
}
// Emitted when `citizen` successfully calls `Join`.
type Joined struct {
member std.Address
}
// Emitted when `citizen` successfully calls `Leave`.
type Left struct {
member std.Address
}
iproposal.gno
:
type IProposal interface {
// Returns the `proposalValue`. Currently just a uint64 value
Value() uint64
// Votes on a proposal (must be DAO `member`)
Vote(voter_ std.Address, vote_ int8) bool
// Ends a proposal and returns final vote (must be DAO `admin`)
Resolve() int8
}
// Emitted when `admin` calls `Start`.
type ProposalCreated struct {
proposalId uint64
}
// Emitted when citizen casts a `Vote`.
type Voted struct {
proposalId uint64
member std.Address
}
// Emitted when `Resolve` of `Proposal` completed.
type ProposalResolved struct {
proposalId uint64
finalVote uint8
}
Worth noting, while event types are defined, atm they're not being used.
The Implementations
Given the DAO membership has to be taken care of first it seems the best place to start. Keeping it simple, this version will just track an avl.Tree
mapping address=>bool. I'm sure this will change in future iterations.
First, the important bits of the dao.DAO
implementation of IDAO
. Below is the struct
type definition, along with the NewDAO
package function that will be called by the realm. It also includes the Join
function, with the other functions working similarly. Note: the enforceIdentity
code is commented out in the function, but the enforceAdmin
utility function has been included here.
// TODO: ideally instead of the Struct, identityRegistry should store interface type? noodle
type DAO struct {
admin std.Address
identityRegistry *identity.IdentityRegistry
proposalIdx uint64
proposals avl.Tree
members avl.Tree
}
func NewDAO(admin_ std.Address, registry_ *identity.IdentityRegistry) *DAO{
enforceIdentity(registry_, admin_)
return &DAO{
admin: admin_,
identityRegistry: registry_,
proposalIdx: 0,
proposals: avl.Tree{},
members: avl.Tree{},
}
}
// Joins the DAO (must be a valid `citizen`)
func (d *DAO) Join(address_ std.Address) bool {
enforceIdentity(d.identityRegistry, address_)
addressStr := string(address_)
if d.members.Has(addressStr) {
return false
}
d.members.Set(addressStr,true)
return true
}
---
func (d *DAO) enforceAdmin(caller_ std.Address) {
if caller_ != d.admin {
panic("Must be admin")
}
}
Next is the dao.Proposal
implementation of IProposal
. It's been included entirely (except imports) because it's both small, and the most important part for our testing.
type Proposal struct {
id uint64
dao *DAO
proposer std.Address
proposalValue uint64
voters avl.Tree
yays uint64
nays uint64
undecideds uint64
}
func NewProposal(dao_ *DAO, proposalIdx_ uint64, proposer_ std.Address, value_ uint64) *Proposal{
return &Proposal{
id: proposalIdx_,
dao: dao_,
proposer: proposer_,
proposalValue: value_,
voters: avl.Tree{},
yays: 0,
nays: 0,
undecideds: 0,
}
}
func (p *Proposal) Value() uint64 { return p.proposalValue }
// Casts Vote for the proposal
func (p *Proposal) Vote(voter_ std.Address, vote_ int8) bool {
if !p.dao.Membership(voter_){
panic("Must be a member to vote.")
}
_voterStr := string(voter_)
if p.voters.Has(_voterStr) {
panic("Vote already cast. Can only vote once per-Proposal.")
}
if vote_ < -1 || vote_ > 1 {
panic("Invalid vote value.")
}
p.voters.Set(_voterStr,true)
switch vote_ {
case -1:
p.nays++
case 1:
p.yays++
default:
p.undecideds++
}
return true
}
// Stops the proposal, returning the final vote (balanced ternary)
func (p *Proposal) Resolve() int8 {
_totalVotes := (p.yays + p.nays + p.undecideds).(uint64)
if _totalVotes != p.voters.Size() {
panic(ufmt.Sprintf("Vote/Voter count mismatch. Votes: %d; Voters: %d",_totalVotes, p.voters.Size()))
}
_holdRound := math.Ceil((float64(_totalVotes) * 2 / 3))
_threshhold := uint64(_holdRound)
// no votes = fails
if _totalVotes == 0 {
return -1
}
// missing votes = undecided
if _totalVotes < p.dao.TotalMembers() {
return 0
}
// return value if 2/3 threshhold is met, otherwise undecided
if p.yays >= _threshhold {
return 1
} else if p.nays >= _threshhold {
return -1
} else {
return 0
}
}
That covers the dao
package. Onto the commune
realm to use it.
The Realm
The commune
realm is where the DAO interactions live. I've included the import because the IdentityRegistry
is used from the citizen
realm during init
, but not actually leaned on after commenting out the enforceIdentity
block in the DAO
implementation.
Similar to the foo20
implementation, the admin
and commune
variables are defined, and then initialized in init
. It then auto-joins the admin
.
package commune
import (
"std"
"strings"
"gno.land/p/demo/dao"
"gno.land/p/demo/ufmt"
"gno.land/r/demo/citizen"
)
var (
admin std.Address = "g138j0um48nm4ppcz0tr6az4qu3yjwjl3l6ntqdx" // TODO: helper to change admin
commune *dao.DAO
)
func init() {
commune = dao.NewDAO(admin,citizen.Registry())
commune.Join(admin)
}
----
func enforceAdmin(caller_ std.Address) {
if caller_ != admin {
panic("Must be admin")
}
}
The rest is mostly just delegate functions from the realm exposed functions of Join
, Leave
, Member
, ProposeChange
, ProposalValue
, Proposalresolve
and Vote
.
The Tests
The tests are still only half cooked, since I'll be circling back to the identity registry element and then adding more coverage. For the moment it's limited to public facing coverage found in commune_test
of the realm. But the tests show the following steps passing (and then erroring just to see the log):
Confirm
admin
is auto-joinedConfirm a non-member is not a member
Confirm a non-member joining Joins
Confirm joined member is joined
Confirm joined member cannot join
Adds a third member to test 2/3 threshold
Creates a new proposal from the
admin
accountCasts votes (commented out to let them be toggled on/off
Resolves a Vote with the expected results
const admin std.Address = "g138j0um48nm4ppcz0tr6az4qu3yjwjl3l6ntqdx"
const gnoadmin std.Address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"
const manfred std.Address = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq"
type BooleanTest struct {
name string
response bool
fn func() bool
}
// Check that DAO membership calls respond as expected. Register 3 Citizens to test 2/3 threshhold.
{
std.TestSetOrigCaller(admin)
membershipTests := []BooleanTest{
{"Admin Auto Member", true, func() bool { return Member(admin) }},
{"Non Member Is False", false, func() bool { return Member(gnoadmin) }},
{"Non Member 1 Joins", true, func() bool {
std.TestSetOrigCaller(gnoadmin)
return Join()
}},
{"Joined Is Member", true, func() bool { return Member(gnoadmin) }},
{"Joined Cannot Join", false, func() bool {
std.TestSetOrigCaller(gnoadmin)
return Join()
}},
{"Non Member 2 Joins", true, func() bool {
std.TestSetOrigCaller(manfred)
return Join()
}},
}
for _, tc := range membershipTests {
tr := tc.fn()
if tr != tc.response {
t.Errorf("%s: have: %t want: %t", tc.name, tr, tc.response)
}else{
t.Logf("%s: PASSED; have: %t, want %t",tc.name, tr, tc.response)
}
}
}
// Check that DAO Proposal/Vote calls respond as expected.
{
std.TestSetOrigCaller(admin)
_propId := ProposeChange(9002)
t.Logf("Proposal %d Created", _propId)
// Vote(_propId.(uint64),1)
// t.Logf("Admin Voted %d on %d", 1, _propId)
// std.TestSetOrigCaller(gnoadmin)
// Vote(_propId.(uint64),-1)
// t.Logf("Gnoadmin Voted %d on %d", -1, _propId)
// std.TestSetOrigCaller(manfred)
// Vote(_propId.(uint64),0)
// t.Logf("Manfred Voted %d on %d", 0, _propId)
std.TestSetOrigCaller(admin)
finalVote := ProposalResolve(_propId)
t.Logf("Final Vote: %d", finalVote)
t.Errorf("SUCCESS! FAILING NOW!")
}
Wrap-Up & Next Steps
And that is a toy DAO in Gno... a very toy DAO lol. But it gives me the start of something to play with as I bake more of the Commonwealth's "Commune of Communes" feature set, and perhaps enable DAO-of-DAOs more generally. The full code is available at the commune branch of my fork.
While I'm not entirely decided on it, I'd really like to roll the Federation ability in early so next pass I may focus on roughing out a better take on the culled identity component and perhaps begin defining an OrganicIdentity
and OrganizationalIdentity
construct to lean into that early.