Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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 := proposals.Get(strconv.Itoa(id))
154	if v == nil {
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}