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

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}