tellers.gno
5.32 Kb · 206 lines
1package grc721
2
3import (
4 "chain"
5)
6
7// CallerTeller resolves the acting account at each write as rlm.Previous() —
8// the realm that crossed into the realm holding the teller.
9//
10// SECURITY: this accessor hangs off *PrivateLedger, not *Token, and that is
11// load-bearing. A frame-relative teller acts as whoever crossed into its
12// holder, so it is only meaningful inside the token's own realm, where the
13// wrappers act for a caller who knowingly invoked the collection. Anywhere
14// else it is a confused deputy: a realm a user merely calls could move that
15// user's tokens. The *Token pointer is published — exported vars, the
16// collection facade, grc721reg — while the ledger is not, since NewToken hands
17// it to the creating realm and nowhere else. So a foreign realm cannot mint
18// one.
19//
20// Construction privacy alone is not enough: a realm may legally build a teller
21// and then export the VALUE. The write methods therefore also verify that the
22// invoking realm is the token's own (see guardHome), which leaves a leaked
23// teller inert everywhere but home.
24func (led *PrivateLedger) CallerTeller() Teller {
25 if led == nil {
26 panic("Ledger cannot be nil")
27 }
28
29 return &fnTeller{
30 accountFn: func(_ int, rlm realm) address {
31 return rlm.Previous().Address()
32 },
33 homeGuard: true,
34 Token: led.token,
35 }
36}
37
38// Write methods return ErrReadonly.
39func (tok *Token) ReadonlyTeller() Teller {
40 if tok == nil {
41 panic("Token cannot be nil")
42 }
43
44 return &fnTeller{
45 accountFn: nil,
46 Token: tok,
47 }
48}
49
50// Permanently acts as the calling realm (verified via IsCurrent).
51func (tok *Token) RealmTeller(_ int, rlm realm) Teller {
52 if tok == nil {
53 panic("Token cannot be nil")
54 }
55
56 if !rlm.IsCurrent() {
57 panic(ErrSpoofedRealm)
58 }
59
60 caller := rlm.Address()
61
62 return &fnTeller{
63 accountFn: func(_ int, _ realm) address {
64 return caller
65 },
66 Token: tok,
67 }
68}
69
70// Like RealmTeller but acts as a sub-account derived from slug.
71func (tok *Token) RealmSubTeller(_ int, rlm realm, slug string) Teller {
72 if tok == nil {
73 panic("Token cannot be nil")
74 }
75
76 if !rlm.IsCurrent() {
77 panic(ErrSpoofedRealm)
78 }
79
80 account := accountSlugAddr(rlm.Address(), slug)
81
82 return &fnTeller{
83 accountFn: func(_ int, _ realm) address {
84 return account
85 },
86 Token: tok,
87 }
88}
89
90// Admin-grade: issuer-only (holds the PrivateLedger). Panics on invalid addr to close the empty-sentinel hole.
91func (led *PrivateLedger) ImpersonateTeller(addr address) Teller {
92 if led == nil {
93 panic("Ledger cannot be nil")
94 }
95
96 if !addr.IsValid() {
97 panic(ErrInvalidAddress)
98 }
99
100 return &fnTeller{
101 accountFn: func(_ int, _ realm) address {
102 return addr
103 },
104 Token: led.token,
105 }
106}
107
108// guardHome confines a frame-relative teller to the token's own realm.
109// Construction privacy stops a foreign realm from minting one; this stops a
110// minted one from travelling, which is what happens when a realm legally builds
111// a teller and then exports the value.
112//
113// The check is on the invoking realm's path alone — deliberately NOT on whether
114// the resolved actor is an end user. Keying on the actor only blocks the case
115// where the debited party is the signing user, and leaves two doors open: a
116// realm can be charged by a realm it calls, and TransferFrom resolves the
117// *spender* from the frame, so a realm reached from an honest hub spends that
118// hub's approvals against any owner who granted one. Both are the same defect
119// as the original one level up — frame-relative resolution means whoever you
120// call can act as you — and neither is reachable once the teller only works at
121// home.
122//
123// The host is compared after stripping any ":subpath" synthesized by realm.Sub,
124// so the token's own sub-realms are not falsely rejected.
125//
126// A foreign realm that needs to move a user's tokens uses the ordinary route:
127// the owner Approves it, and it spends as itself through RealmTeller, which is
128// eagerly bound to its own address and approval-gated.
129//
130// The leading int keeps this a plain method.
131func (ft *fnTeller) guardHome(_ int, rlm realm) error {
132 if !ft.homeGuard {
133 return nil
134 }
135
136 host, _, _ := chain.SplitPkgSubPath(rlm.PkgPath())
137 if host != ft.Token.origRealm {
138 return ErrForeignCallerTeller
139 }
140
141 return nil
142}
143
144func (ft *fnTeller) Approve(_ int, rlm realm, to address, tid TokenID) error {
145 if ft.accountFn == nil {
146 return ErrReadonly
147 }
148
149 if !rlm.IsCurrent() {
150 return ErrSpoofedRealm
151 }
152
153 if err := ft.guardHome(0, rlm); err != nil {
154 return err
155 }
156
157 caller := ft.accountFn(0, rlm)
158
159 return ft.Token.ledger.Approve(caller, to, tid)
160}
161
162func (ft *fnTeller) SetApprovalForAll(_ int, rlm realm, operator address, approved bool) error {
163 if ft.accountFn == nil {
164 return ErrReadonly
165 }
166
167 if !rlm.IsCurrent() {
168 return ErrSpoofedRealm
169 }
170
171 if err := ft.guardHome(0, rlm); err != nil {
172 return err
173 }
174
175 caller := ft.accountFn(0, rlm)
176
177 return ft.Token.ledger.SetApprovalForAll(caller, operator, approved)
178}
179
180func (ft *fnTeller) TransferFrom(_ int, rlm realm, from, to address, tid TokenID) error {
181 if ft.accountFn == nil {
182 return ErrReadonly
183 }
184
185 if !rlm.IsCurrent() {
186 return ErrSpoofedRealm
187 }
188
189 if err := ft.guardHome(0, rlm); err != nil {
190 return err
191 }
192
193 spender := ft.accountFn(0, rlm)
194
195 return ft.Token.ledger.TransferFrom(spender, from, to, tid)
196}
197
198func accountSlugAddr(addr address, slug string) address {
199 if slug == "" {
200 return addr
201 }
202
203 key := addr.String() + "/" + slug
204
205 return chain.PackageAddress(key)
206}