tellers.gno
6.80 Kb · 216 lines
1package grc20
2
3import (
4 "chain"
5)
6
7// CallerTeller returns a GRC20 compatible teller that, at each write call,
8// resolves the caller as rlm.Previous() — the realm that crossed into the
9// caller. rlm must be the caller's own captured cur (asserted via
10// rlm.IsCurrent() inside the Teller methods).
11//
12// SECURITY: this accessor hangs off *PrivateLedger, not *Token, and that is
13// load-bearing. A frame-relative teller debits whoever crossed into the realm
14// holding it, so it is only ever meaningful inside the token's own realm,
15// whose wrappers act for a caller who knowingly invoked the token. Anywhere
16// else it is a confused deputy. The *Token pointer is published (exported
17// vars, grc20factory, grc20reg) but the ledger is not — NewToken hands it to
18// the creating realm and nowhere else — so a foreign realm cannot mint 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 makes a leaked
23// teller inert everywhere but home.
24func (ledger *PrivateLedger) CallerTeller() Teller {
25 if ledger == 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: ledger.token,
35 }
36}
37
38// ReadonlyTeller is a GRC20 compatible teller that panics for any write operation.
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// RealmTeller returns a GRC20 compatible teller that will store the
51// caller realm permanently. Calling anything through this teller will
52// result in allowance or balance changes for the realm that initialized the teller.
53// The initializer of this teller should usually never share the resulting Teller from
54// this method except maybe for advanced delegation flows such as a DAO treasury
55// management.
56//
57// rlm must be the caller's own captured cur (asserted via rlm.IsCurrent()).
58// The address is frozen eagerly at construction.
59func (tok *Token) RealmTeller(_ int, rlm realm) Teller {
60 if tok == nil {
61 panic("Token cannot be nil")
62 }
63 if !rlm.IsCurrent() {
64 panic(ErrSpoofedRealm)
65 }
66
67 caller := rlm.Address()
68
69 return &fnTeller{
70 accountFn: func(_ int, _ realm) address {
71 return caller
72 },
73 Token: tok,
74 }
75}
76
77// RealmSubTeller is like RealmTeller but uses the provided slug to derive a
78// subaccount.
79//
80// rlm must be the caller's own captured cur (asserted via rlm.IsCurrent()).
81// The subaccount address is frozen eagerly at construction.
82func (tok *Token) RealmSubTeller(_ int, rlm realm, slug string) Teller {
83 if tok == nil {
84 panic("Token cannot be nil")
85 }
86 if !rlm.IsCurrent() {
87 panic(ErrSpoofedRealm)
88 }
89
90 account := accountSlugAddr(rlm.Address(), slug)
91
92 return &fnTeller{
93 accountFn: func(_ int, _ realm) address {
94 return account
95 },
96 Token: tok,
97 }
98}
99
100// ImpersonateTeller returns a GRC20 compatible teller that impersonates as a
101// specified address. This allows operations to be performed as if they were
102// executed by the given address, enabling the caller to manipulate tokens on
103// behalf of that address.
104//
105// It is particularly useful in scenarios where a contract needs to perform
106// actions on behalf of a user or another account, without exposing the
107// underlying logic or requiring direct access to the user's account. The
108// returned teller will use the provided address for all operations, effectively
109// masking the original caller.
110//
111// This method should be used with caution, as it allows for potentially
112// sensitive operations to be performed under the guise of another address.
113func (ledger *PrivateLedger) ImpersonateTeller(addr address) Teller {
114 if ledger == nil {
115 panic("Ledger cannot be nil")
116 }
117
118 return &fnTeller{
119 accountFn: func(_ int, _ realm) address {
120 return addr
121 },
122 Token: ledger.token,
123 }
124}
125
126// generic tellers methods.
127//
128
129// guardHome confines a frame-relative (homeGuard) teller to the token's own
130// realm. Construction privacy stops a foreign realm from minting one;
131// this stops a minted one from travelling, which is what happens when a realm
132// legally builds a teller and then exports the value.
133//
134// The check is on the invoking realm's path alone — deliberately NOT on
135// whether the resolved actor is an end user. Keying on the actor only blocks
136// the case where the debited party is the signing user, which leaves two doors
137// open: a realm can be charged by a realm it calls, and TransferFrom resolves
138// the *spender* from the frame, so a realm reached from an honest hub spends
139// that hub's allowance against any owner who granted one. Both are the same
140// defect as the original one level up — frame-relative resolution means
141// whoever you call can act as you — and neither is reachable once the teller
142// only works at home.
143//
144// The host is compared after stripping any ":subpath" synthesized by
145// realm.Sub, so the token's own sub-realms are not falsely rejected.
146//
147// A foreign realm that needs to move a user's funds uses the ordinary route:
148// Approve, then RealmTeller().TransferFrom, which is eagerly bound to that
149// realm's own address and allowance-gated.
150//
151// The leading int keeps this a plain method.
152func (ft *fnTeller) guardHome(_ int, rlm realm) error {
153 if !ft.homeGuard {
154 return nil
155 }
156 host, _, _ := chain.SplitPkgSubPath(rlm.PkgPath())
157 if host != ft.Token.origRealm {
158 return ErrForeignCallerTeller
159 }
160 return nil
161}
162
163func (ft *fnTeller) Transfer(_ int, rlm realm, to address, amount int64) error {
164 if ft.accountFn == nil {
165 return ErrReadonly
166 }
167 if !rlm.IsCurrent() {
168 return ErrSpoofedRealm
169 }
170 if err := ft.guardHome(0, rlm); err != nil {
171 return err
172 }
173 caller := ft.accountFn(0, rlm)
174 return ft.Token.ledger.Transfer(caller, to, amount)
175}
176
177func (ft *fnTeller) Approve(_ int, rlm realm, spender address, amount int64) error {
178 if ft.accountFn == nil {
179 return ErrReadonly
180 }
181 if !rlm.IsCurrent() {
182 return ErrSpoofedRealm
183 }
184 if err := ft.guardHome(0, rlm); err != nil {
185 return err
186 }
187 caller := ft.accountFn(0, rlm)
188 return ft.Token.ledger.Approve(caller, spender, amount)
189}
190
191func (ft *fnTeller) TransferFrom(_ int, rlm realm, owner, to address, amount int64) error {
192 if ft.accountFn == nil {
193 return ErrReadonly
194 }
195 if !rlm.IsCurrent() {
196 return ErrSpoofedRealm
197 }
198 if err := ft.guardHome(0, rlm); err != nil {
199 return err
200 }
201 spender := ft.accountFn(0, rlm)
202 return ft.Token.ledger.TransferFrom(owner, spender, to, amount)
203}
204
205// helpers
206//
207
208// accountSlugAddr returns the address derived from the specified address and slug.
209func accountSlugAddr(addr address, slug string) address {
210 // XXX: use a new `std.XXX` call for this.
211 if slug == "" {
212 return addr
213 }
214 key := addr.String() + "/" + slug
215 return chain.PackageAddress(key) // temporarily using this helper
216}