Search Apps Documentation Source Content File Folder Download Copy Actions Download

multisig.gno

5.49 Kb · 223 lines
  1// Package multisig implements an M-of-N approval multisig realm.
  2//
  3// It is a port of the classic Solidity multisig idea: a fixed set of owners
  4// is configured once, any owner may create a proposal, owners approve it, and
  5// once the number of distinct approvals reaches the threshold the proposal is
  6// marked "Executed". This realm tracks approvals only — it does not move funds.
  7package multisig
  8
  9import (
 10	"errors"
 11	"strconv"
 12	"strings"
 13
 14	"chain"
 15	"chain/runtime/unsafe"
 16
 17	"gno.land/p/nt/avl/v0"
 18)
 19
 20// State of a single proposal.
 21type stateKind int
 22
 23const (
 24	statePending stateKind = iota
 25	stateExecuted
 26)
 27
 28func (s stateKind) String() string {
 29	if s == stateExecuted {
 30		return "Executed"
 31	}
 32	return "Pending"
 33}
 34
 35// proposal is a single approval request.
 36type proposal struct {
 37	id          int
 38	description string
 39	proposer    address
 40	approvals   *avl.Tree // owner address string -> struct{}{}, distinct approvers
 41	state       stateKind
 42}
 43
 44// Package-level persistent state.
 45var (
 46	owners      *avl.Tree // owner address string -> struct{}{}
 47	ownerCount  int
 48	threshold   int
 49	initialized bool
 50
 51	proposals   *avl.Tree // decimal id string -> *proposal
 52	nextID      int
 53)
 54
 55var (
 56	errAlreadyInit = errors.New("multisig: already initialized")
 57	errNotInit     = errors.New("multisig: not initialized")
 58	errBadThreshold = errors.New("multisig: threshold must be between 1 and number of owners")
 59	errNoOwners    = errors.New("multisig: at least one owner required")
 60	errNotOwner    = errors.New("multisig: caller is not an owner")
 61	errNoProposal  = errors.New("multisig: proposal not found")
 62	errDupApproval = errors.New("multisig: caller already approved this proposal")
 63	errExecuted    = errors.New("multisig: proposal already executed")
 64)
 65
 66func init() {
 67	owners = avl.NewTree()
 68	proposals = avl.NewTree()
 69}
 70
 71// caller returns the address of the realm/user that called into this realm.
 72func caller() address {
 73	return unsafe.PreviousRealm().Address()
 74}
 75
 76func isOwner(a address) bool {
 77	return owners.Has(a.String())
 78}
 79
 80// Setup configures the multisig exactly once. owners is a comma-separated list
 81// of bech32 addresses; threshold is the number of approvals required to execute
 82// a proposal. It panics (cross-realm abort) on invalid input or if already set.
 83func Setup(cur realm, ownersCSV string, thresholdN int) {
 84	if initialized {
 85		panic(errAlreadyInit)
 86	}
 87
 88	parts := strings.Split(ownersCSV, ",")
 89	count := 0
 90	for _, p := range parts {
 91		a := strings.TrimSpace(p)
 92		if a == "" {
 93			continue
 94		}
 95		addr := address(a)
 96		if owners.Has(addr.String()) {
 97			continue // dedupe
 98		}
 99		owners.Set(addr.String(), struct{}{})
100		count++
101	}
102	if count == 0 {
103		panic(errNoOwners)
104	}
105	if thresholdN < 1 || thresholdN > count {
106		panic(errBadThreshold)
107	}
108
109	ownerCount = count
110	threshold = thresholdN
111	initialized = true
112
113	chain.Emit("Setup", "owners", strconv.Itoa(count), "threshold", strconv.Itoa(thresholdN))
114}
115
116// Propose creates a new proposal authored by an owner and returns its id.
117func Propose(cur realm, description string) int {
118	if !initialized {
119		panic(errNotInit)
120	}
121	c := caller()
122	if !isOwner(c) {
123		panic(errNotOwner)
124	}
125
126	id := nextID
127	nextID++
128
129	p := &proposal{
130		id:          id,
131		description: description,
132		proposer:    c,
133		approvals:   avl.NewTree(),
134		state:       statePending,
135	}
136	proposals.Set(strconv.Itoa(id), p)
137
138	chain.Emit("Propose", "id", strconv.Itoa(id), "proposer", c.String())
139	return id
140}
141
142// Approve records an owner's approval for a proposal. When the count of distinct
143// approvals reaches the threshold, the proposal becomes Executed.
144func Approve(cur realm, id int) {
145	if !initialized {
146		panic(errNotInit)
147	}
148	c := caller()
149	if !isOwner(c) {
150		panic(errNotOwner)
151	}
152
153	v, ok := proposals.Get(strconv.Itoa(id))
154	if !ok {
155		panic(errNoProposal)
156	}
157	p := v.(*proposal)
158	if p.state == stateExecuted {
159		panic(errExecuted)
160	}
161	if p.approvals.Has(c.String()) {
162		panic(errDupApproval)
163	}
164
165	p.approvals.Set(c.String(), struct{}{})
166	chain.Emit("Approve", "id", strconv.Itoa(id), "owner", c.String(), "approvals", strconv.Itoa(p.approvals.Size()))
167
168	if p.approvals.Size() >= threshold {
169		p.state = stateExecuted
170		chain.Emit("Executed", "id", strconv.Itoa(id))
171	}
172}
173
174// Render returns a Markdown view of the multisig configuration and proposals.
175func Render(path string) string {
176	var b strings.Builder
177	b.WriteString("# M-of-N Multisig\n\n")
178
179	if !initialized {
180		b.WriteString("_Not initialized. Call `Setup(owners, threshold)` once._\n")
181		return b.String()
182	}
183
184	b.WriteString("**Threshold:** ")
185	b.WriteString(strconv.Itoa(threshold))
186	b.WriteString(" of ")
187	b.WriteString(strconv.Itoa(ownerCount))
188	b.WriteString("\n\n## Owners\n\n")
189	owners.Iterate("", "", func(key string, _ interface{}) bool {
190		b.WriteString("- `")
191		b.WriteString(key)
192		b.WriteString("`\n")
193		return false
194	})
195
196	b.WriteString("\n## Proposals\n\n")
197	if proposals.Size() == 0 {
198		b.WriteString("_No proposals yet._\n")
199		return b.String()
200	}
201
202	b.WriteString("| ID | Description | Proposer | Approvals | State |\n")
203	b.WriteString("|----|-------------|----------|-----------|-------|\n")
204	proposals.Iterate("", "", func(_ string, v interface{}) bool {
205		p := v.(*proposal)
206		b.WriteString("| ")
207		b.WriteString(strconv.Itoa(p.id))
208		b.WriteString(" | ")
209		b.WriteString(p.description)
210		b.WriteString(" | `")
211		b.WriteString(p.proposer.String())
212		b.WriteString("` | ")
213		b.WriteString(strconv.Itoa(p.approvals.Size()))
214		b.WriteString("/")
215		b.WriteString(strconv.Itoa(threshold))
216		b.WriteString(" | ")
217		b.WriteString(p.state.String())
218		b.WriteString(" |\n")
219		return false
220	})
221
222	return b.String()
223}