pairreg.gno
6.18 Kb · 192 lines
1// Package pairreg is the registry every AMM pair instance announces itself to,
2// and the unified frontend over all of them.
3//
4// It is the third artifact of the instance-per-realm pattern: one shared
5// p/moul/x/pair/v0 holding the logic, N tiny realms holding one pair each, and
6// this realm knowing all of them.
7//
8// # Why registration cannot be faked, and code cannot be trusted
9//
10// Register keys an instance by cur.Previous().PkgPath(), which the runtime
11// supplies, so an instance cannot claim a path that is not its own. That is
12// the whole on-chain guarantee. It is NOT a guarantee about the code at that
13// path: gno has no way to read another package's source from inside a realm,
14// so nothing here can check that an instance really runs the shared template.
15// The mirror image of Ethereum's CREATE2, which proves the code and says
16// nothing legible about who deployed it.
17//
18// Consequences a reader of the index must keep in mind:
19//
20// - Reserves shown here are CLAIMED. They are read live from the instance's
21// own struct, which is honest for an instance running the template and
22// arbitrary for one that does not.
23// - Several instances may exist for the same token couple. That is allowed
24// on purpose; there is no canonical pair and no factory to enforce one.
25// - Approving tokens to an instance risks exactly what was approved. Read
26// the instance's source (vm/qfile on its path) before funding it.
27//
28// # Why the registry can read live state at all
29//
30// Instances register a *pair.Pair pointer, not an address, the way grc20reg
31// registers a *grc20.Token. One vm/qrender here therefore renders the real
32// state of every instance, with no indexer and no multicall. It is safe
33// because pair.Pair holds only concrete types: an interface or a func field
34// would let a hostile instance hand foreign code to this realm's frame.
35package pairreg
36
37import (
38 "chain"
39 "chain/runtime"
40 "strings"
41
42 "gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/md/v0"
43 "gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/pair/v0"
44 "gno.land/p/nt/avl/v0"
45 "gno.land/p/nt/ufmt/v0"
46)
47
48// Entry is one registered instance.
49type Entry struct {
50 Path string // the instance realm's package path, supplied by the runtime
51 Pair *pair.Pair // live pointer into the instance's own storage
52 Height int64 // chain height at registration
53}
54
55// entries maps instance path -> *Entry. couples maps a pair ID
56// ("keyA~keyB") -> int, how many instances claim that couple.
57var (
58 entries avl.Tree
59 couples avl.Tree
60)
61
62// Register records the calling realm as an instance. Call it from the
63// instance's init, so that deploying and listing are one transaction:
64//
65// func init(cur realm) {
66// p = pair.New(keyA, keyB, grc20reg.MustGet(keyA), grc20reg.MustGet(keyB))
67// pairreg.Register(cross(cur), p)
68// }
69//
70// Permissionless by design: anyone may deploy an instance under their own
71// address namespace and land here. Spam is self funded, since the registering
72// transaction pays for the storage it adds.
73func Register(cur realm, p *pair.Pair) {
74 caller := cur.Previous()
75 path := caller.PkgPath()
76 if path == "" {
77 panic("pairreg: only a realm can register")
78 }
79 if p == nil {
80 panic("pairreg: nil pair")
81 }
82 if entries.Has(path) {
83 panic("pairreg: already registered: " + path)
84 }
85
86 entries.Set(path, &Entry{Path: path, Pair: p, Height: runtime.ChainHeight()})
87 couples.Set(p.ID(), countFor(p.ID())+1)
88
89 chain.Emit("RegisterPair",
90 "path", path,
91 "pair", p.ID(),
92 )
93}
94
95// Get returns the entry for an instance path, or nil.
96func Get(path string) *Entry {
97 v := entries.Get(path)
98 if v == nil {
99 return nil
100 }
101 return v.(*Entry)
102}
103
104// Size returns how many instances are registered.
105func Size() int { return entries.Size() }
106
107// Couples returns how many distinct token couples are represented.
108func Couples() int { return couples.Size() }
109
110// InstancesFor returns the instance paths claiming a given couple id.
111func InstancesFor(id string) []string {
112 out := []string{}
113 entries.Iterate("", "", func(_ string, value any) bool {
114 e := value.(*Entry)
115 if e.Pair.ID() == id {
116 out = append(out, e.Path)
117 }
118 return false
119 })
120 return out
121}
122
123// Render is the unified frontend: the index of every instance with its live
124// state, or one instance's own page when path is an instance path.
125func Render(path string) string {
126 if path != "" {
127 e := Get(path)
128 if e == nil {
129 return "# 404\n\nNo instance registered at " + md.InlineCode(path) + ".\n"
130 }
131 out := e.Pair.Render("")
132 out += "\n---\n\n"
133 out += ufmt.Sprintf("Instance: [`%s`](%s) · registered at height %d · %d instance(s) claim this couple.\n",
134 e.Path, webPath(e.Path), e.Height, countFor(e.Pair.ID()))
135 return out
136 }
137
138 out := "# AMM pairs\n\n"
139 out += "One realm per token couple, all running [p/moul/x/pair/v0](/p/moul/x/pair/v0). "
140 out += "Anyone can deploy one under their own namespace and it lands here.\n\n"
141
142 if entries.Size() == 0 {
143 out += "_No instance yet._\n"
144 return out
145 }
146
147 out += ufmt.Sprintf("%d instance(s), %d couple(s).\n\n", entries.Size(), couples.Size())
148 out += "**Reserves below are claimed by each instance, not verified.** "
149 out += "Nothing on chain can check that an instance runs the shared template; read its source before funding it.\n\n"
150 out += "| pair | reserves | LP shares | providers | instance |\n"
151 out += "|---|---|---|---|---|\n"
152 entries.Iterate("", "", func(key string, value any) bool {
153 e := value.(*Entry)
154 symA, symB := e.Pair.Symbols()
155 resA, resB := e.Pair.Reserves()
156 out += ufmt.Sprintf("| [%s/%s](/r/moul/x/pairreg/v0:%s) | %d %s / %d %s | %d | %d | [`%s`](%s) |\n",
157 symA, symB, key,
158 resA, symA, resB, symB,
159 e.Pair.TotalShares(), e.Pair.Providers(),
160 shortPath(e.Path), webPath(e.Path))
161 return false
162 })
163 return out
164}
165
166//
167// Internals.
168//
169
170func countFor(id string) int {
171 v := couples.Get(id)
172 if v == nil {
173 return 0
174 }
175 return v.(int)
176}
177
178// webPath turns a package path into a gnoweb link.
179func webPath(pkgPath string) string {
180 if i := strings.Index(pkgPath, "/"); i >= 0 {
181 return pkgPath[i:]
182 }
183 return "/" + pkgPath
184}
185
186// shortPath drops the chain domain, which is the same on every row.
187func shortPath(pkgPath string) string {
188 if i := strings.Index(pkgPath, "/"); i >= 0 {
189 return pkgPath[i+1:]
190 }
191 return pkgPath
192}