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

grc20reg_test.gno

12.48 Kb · 316 lines
  1package grc20reg
  2
  3import (
  4	"chain"
  5	"strings"
  6	"testing"
  7
  8	"gno.land/p/nt/grc20/v0"
  9	"gno.land/p/nt/testutils/v0"
 10	"gno.land/p/nt/uassert/v0"
 11	"gno.land/p/nt/urequire/v0"
 12)
 13
 14func TestRegistry(cur realm, t *testing.T) {
 15	testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/foo"))
 16	token, ledger := grc20.NewToken("TestToken", "TST", 4, 0, cur)
 17	ledger.Mint(cur.Address(), 1234567)
 18	// register
 19	key := Register(cross(cur), token, "mySlug")
 20	regToken := Get(key)
 21	urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil
 22	urequire.Equal(t, regToken.GetSymbol(), "TST")
 23
 24	expected := `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo).TST - [info](/r/nt/grc20reg/v0:gno.land/r/demo/foo.TST)
 25`
 26	got := Render("")
 27	urequire.True(t, strings.Contains(got, expected))
 28	// 404
 29	invalidToken := Get("0xdeadbeef")
 30	urequire.True(t, invalidToken == nil)
 31
 32	got = Render("")
 33	urequire.True(t, strings.Contains(got, expected))
 34
 35	expected = `# TestToken
 36- symbol: **TST**
 37- realm: [gno.land/r/demo/foo](/r/demo/foo).TST
 38- decimals: 4
 39- total supply: 1234567
 40`
 41	got = Render(key)
 42	urequire.Equal(t, expected, got)
 43
 44	// The registry keys by rlmPath.symbol, so a second token with the same
 45	// symbol in the same realm is rejected even though its Token.ID() differs
 46	// (distinct trailing sequence id). See TestRegisterRejectsOverwrite.
 47	second, _ := grc20.NewToken("Second", "TST", 4, 1, cur)
 48	urequire.NotEqual(t, token.ID(), second.ID()) // ids are decoupled from symbol
 49	urequire.AbortsContains(t, cur, "token already registered", func() {
 50		Register(cross(cur), second, "")
 51	})
 52}
 53
 54// TestRegistryLookupGrantsNoSpendAuthority pins the property that replaced the
 55// former Transfer/Approve/TransferFrom wrappers: a registry lookup yields a
 56// *Token, and a *Token alone carries no authority to debit anybody. The
 57// frame-relative teller is reachable only from the ledger, which never leaves
 58// the token's own realm, so this realm cannot act on behalf of its caller —
 59// that shape was the confused deputy.
 60//
 61// The supported route for a realm that must move a user's funds is the one
 62// exercised below: the user grants an allowance, and the realm spends it as
 63// itself through a RealmTeller, which is eagerly bound to its own address.
 64func TestRegistryLookupGrantsNoSpendAuthority(cur realm, t *testing.T) {
 65	const (
 66		tokenPath    = "gno.land/r/demo/token"
 67		consumerPath = "gno.land/r/demo/grc20reg_consumer"
 68	)
 69	alice := testutils.TestAddress("alice")
 70	bob := testutils.TestAddress("bob")
 71	consumer := chain.PackageAddress(consumerPath)
 72
 73	testing.SetRealm(testing.NewCodeRealm(tokenPath))
 74	token, ledger := grc20.NewToken("TestToken", "TST", 4, 0, cur)
 75	urequire.NoError(t, ledger.Mint(alice, 1_000))
 76	tokenKey := Register(cross(cur), token, "")
 77	uassert.Equal(t, "TestToken", MustGet(tokenKey).GetName())
 78	uassert.Equal(t, int64(1_000), MustGet(tokenKey).BalanceOf(alice))
 79	uassert.Equal(t, int64(0), MustGet(tokenKey).Allowance(alice, consumer))
 80
 81	// alice approves the consumer realm on the token. On chain she does this
 82	// through the token realm's own entry point; the ledger stands in for it.
 83	urequire.NoError(t, ledger.ImpersonateTeller(alice).Approve(0, cur, consumer, 300))
 84	uassert.Equal(t, int64(300), MustGet(tokenKey).Allowance(alice, consumer))
 85
 86	// The consumer spends that allowance as ITSELF, over a token it found in
 87	// the registry and does not own. The actor is fixed at construction, so it
 88	// cannot be redirected by whoever calls in.
 89	testing.SetRealm(testing.NewCodeRealm(consumerPath))
 90	spender := MustGet(tokenKey).RealmTeller(0, cur)
 91	urequire.NoError(t, spender.TransferFrom(0, cur, alice, bob, 200))
 92	uassert.Equal(t, int64(800), MustGet(tokenKey).BalanceOf(alice))
 93	uassert.Equal(t, int64(200), MustGet(tokenKey).BalanceOf(bob))
 94	uassert.Equal(t, int64(100), MustGet(tokenKey).Allowance(alice, consumer))
 95
 96	// Beyond the allowance it stops: the balance is not reachable directly.
 97	uassert.ErrorContains(t,
 98		spender.TransferFrom(0, cur, alice, bob, 101),
 99		"insufficient allowance")
100	uassert.Equal(t, int64(800), MustGet(tokenKey).BalanceOf(alice))
101	uassert.Equal(t, int64(200), MustGet(tokenKey).BalanceOf(bob))
102}
103
104// TestRegistryConsumerCannotRedirectItsActor is the negative half of the
105// property above: a realm holding a registered token pays out of its own
106// balance, not the signing user's. The actor is bound at construction, so the
107// transaction's origin caller has no bearing on who is debited — which is what
108// made the frame-relative teller in a foreign realm a phishing primitive.
109func TestRegistryConsumerCannotRedirectItsActor(cur realm, t *testing.T) {
110	const (
111		tokenPath    = "gno.land/r/demo/token_actor_binding"
112		consumerPath = "gno.land/r/demo/grc20reg_actor_consumer"
113	)
114	alice := testutils.TestAddress("alice")
115	bob := testutils.TestAddress("bob")
116	consumer := chain.PackageAddress(consumerPath)
117
118	testing.SetRealm(testing.NewCodeRealm(tokenPath))
119	token, ledger := grc20.NewToken("ActorBinding", "ACTB", 4, 0, cur)
120	urequire.NoError(t, ledger.Mint(alice, 1_000))
121	urequire.NoError(t, ledger.Mint(consumer, 500))
122	tokenKey := Register(cross(cur), token, "")
123
124	// alice signs the transaction, and holds a balance the consumer would love
125	// to spend. The consumer pays out of its own instead.
126	testing.SetOriginCaller(alice)
127	testing.SetRealm(testing.NewCodeRealm(consumerPath))
128	urequire.NoError(t, MustGet(tokenKey).RealmTeller(0, cur).Transfer(0, cur, bob, 100))
129	uassert.Equal(t, int64(1_000), MustGet(tokenKey).BalanceOf(alice))
130	uassert.Equal(t, int64(400), MustGet(tokenKey).BalanceOf(consumer))
131	uassert.Equal(t, int64(100), MustGet(tokenKey).BalanceOf(bob))
132}
133
134func TestRegisterRejectsOverwrite(cur realm, t *testing.T) {
135	testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/grc20reg_overwrite"))
136	token, ledger := grc20.NewToken("Bar", "BAR", 4, 0, cur)
137	ledger.Mint(cur.Address(), 11)
138	key := Register(cross(cur), token, "")
139
140	urequire.Equal(t, "BAR", Get(key).GetSymbol())
141	urequire.Equal(t, int64(11), Get(key).BalanceOf(cur.Address()))
142
143	replacement, _ := grc20.NewToken("Replacement", "BAR", 6, 0, cur)
144	urequire.AbortsContains(t, cur, "token already registered", func() {
145		Register(cross(cur), replacement, "")
146	})
147}
148
149func TestRegisterRejectsAliasedTokenPaths(cur realm, t *testing.T) {
150	testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/grc20reg_alias"))
151	token, _ := grc20.NewToken("Aliased Token", "ALIAS", 4, 0, cur)
152	Register(cross(cur), token, "first")
153
154	urequire.AbortsContains(t, cur, "token already registered", func() {
155		Register(cross(cur), token, "second")
156	})
157}
158
159func TestRegisterRejectsTokenFromDifferentRealm(cur realm, t *testing.T) {
160	testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/grc20reg_id_source"))
161	token, _ := grc20.NewToken("Mismatch Token", "MISMATCH", 4, 0, cur)
162
163	testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/grc20reg_id_target"))
164	urequire.AbortsContains(t, cur, "token must be registered from its own realm", func() {
165		Register(cross(cur), token, "")
166	})
167}
168
169func TestValidateSlug(cur realm, t *testing.T) {
170	// Valid slugs — should not panic
171	valid := []string{"mytoken", "my-token", "my_token", "Token123", "a", "A-B_c", strings.Repeat("a", maxSlugLen)}
172	for _, slug := range valid {
173		validateSlug(slug) // no panic = pass
174	}
175}
176
177func TestValidateSlugPanicsOnTooLong(cur realm, t *testing.T) {
178	defer func() { recover() }()
179	validateSlug(strings.Repeat("a", maxSlugLen+1))
180	t.Errorf("should have panicked")
181}
182
183func TestValidateSlugPanicsOnSpace(cur realm, t *testing.T) {
184	defer func() { recover() }()
185	validateSlug("has space")
186	t.Errorf("should have panicked")
187}
188
189func TestValidateSlugPanicsOnDot(cur realm, t *testing.T) {
190	defer func() { recover() }()
191	validateSlug("has.dot")
192	t.Errorf("should have panicked")
193}
194
195func TestValidateSlugPanicsOnSlash(cur realm, t *testing.T) {
196	defer func() { recover() }()
197	validateSlug("has/slash")
198	t.Errorf("should have panicked")
199}
200
201func TestValidateSlugPanicsOnBrackets(cur realm, t *testing.T) {
202	defer func() { recover() }()
203	validateSlug("[brackets]")
204	t.Errorf("should have panicked")
205}
206
207func TestValidateSlugPanicsOnParens(cur realm, t *testing.T) {
208	defer func() { recover() }()
209	validateSlug("(parens)")
210	t.Errorf("should have panicked")
211}
212
213func TestValidateSlugPanicsOnInjection(cur realm, t *testing.T) {
214	defer func() { recover() }()
215	validateSlug(`) [Claim](https://evil.com`)
216	t.Errorf("should have panicked")
217}
218
219func TestRegisterRejectsNilToken(cur realm, t *testing.T) {
220	testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/grc20reg_nil"))
221	urequire.AbortsContains(t, cur, "nil token", func() {
222		Register(cross(cur), nil, "")
223	})
224}
225
226// TestWrappersBindActorToCallingRealm pins the property that makes the write
227// wrappers safe: they are non-crossing, so `rlm` reaches RealmTeller as the
228// caller's own live token and the debit lands on the caller. A crossing wrapper
229// would mint a fresh `cur` for grc20reg and spend the registry's balance
230// instead, which is why the convention is not a stylistic choice.
231//
232// Both the consumer and the registry hold a balance, so whichever one is
233// debited is observable rather than inferred.
234func TestWrappersBindActorToCallingRealm(cur realm, t *testing.T) {
235	const (
236		tokenPath    = "gno.land/r/demo/token_wrapper_actor"
237		consumerPath = "gno.land/r/demo/grc20reg_wrapper_consumer"
238	)
239	bob := testutils.TestAddress("bob")
240	consumer := chain.PackageAddress(consumerPath)
241	registry := chain.PackageAddress("gno.land/r/nt/grc20reg/v0")
242
243	testing.SetRealm(testing.NewCodeRealm(tokenPath))
244	token, ledger := grc20.NewToken("WrapperActor", "WRPA", 4, 0, cur)
245	urequire.NoError(t, ledger.Mint(consumer, 1_000))
246	urequire.NoError(t, ledger.Mint(registry, 1_000))
247	tokenKey := Register(cross(cur), token, "")
248
249	testing.SetRealm(testing.NewCodeRealm(consumerPath))
250	Transfer(0, cur, tokenKey, bob, 100)
251
252	uassert.Equal(t, int64(900), MustGet(tokenKey).BalanceOf(consumer))
253	uassert.Equal(t, int64(1_000), MustGet(tokenKey).BalanceOf(registry))
254	uassert.Equal(t, int64(100), MustGet(tokenKey).BalanceOf(bob))
255}
256
257// TestWrapperActorIgnoresSigningUser is the negative half: the signing user has
258// a balance the calling realm would like to spend, and does not lose it. The
259// actor comes from rlm.Address(), so the transaction's origin has no say in who
260// pays — a hub cannot be induced to debit its caller's caller.
261func TestWrapperActorIgnoresSigningUser(cur realm, t *testing.T) {
262	const (
263		tokenPath    = "gno.land/r/demo/token_wrapper_origin"
264		consumerPath = "gno.land/r/demo/grc20reg_wrapper_origin_consumer"
265	)
266	alice := testutils.TestAddress("alice")
267	bob := testutils.TestAddress("bob")
268	consumer := chain.PackageAddress(consumerPath)
269
270	testing.SetRealm(testing.NewCodeRealm(tokenPath))
271	token, ledger := grc20.NewToken("WrapperOrigin", "WRPO", 4, 0, cur)
272	urequire.NoError(t, ledger.Mint(alice, 1_000))
273	urequire.NoError(t, ledger.Mint(consumer, 500))
274	tokenKey := Register(cross(cur), token, "")
275
276	testing.SetOriginCaller(alice)
277	testing.SetRealm(testing.NewCodeRealm(consumerPath))
278	Transfer(0, cur, tokenKey, bob, 100)
279
280	uassert.Equal(t, int64(1_000), MustGet(tokenKey).BalanceOf(alice))
281	uassert.Equal(t, int64(400), MustGet(tokenKey).BalanceOf(consumer))
282}
283
284// TestTransferFromSpendsAllowanceGrantedToCallingRealm records the semantic
285// shift that comes with binding the actor to the caller: the owner must have
286// approved the CALLING REALM, not the signing user. An allowance granted to
287// anyone else does not authorize the wrapper.
288func TestTransferFromSpendsAllowanceGrantedToCallingRealm(cur realm, t *testing.T) {
289	const (
290		tokenPath    = "gno.land/r/demo/token_wrapper_allowance"
291		consumerPath = "gno.land/r/demo/grc20reg_wrapper_allowance_consumer"
292	)
293	alice := testutils.TestAddress("alice")
294	bob := testutils.TestAddress("bob")
295	consumer := chain.PackageAddress(consumerPath)
296
297	testing.SetRealm(testing.NewCodeRealm(tokenPath))
298	token, ledger := grc20.NewToken("WrapperAllowance", "WRPL", 4, 0, cur)
299	urequire.NoError(t, ledger.Mint(alice, 1_000))
300	tokenKey := Register(cross(cur), token, "")
301
302	// No allowance yet: the wrapper cannot touch alice's balance.
303	testing.SetRealm(testing.NewCodeRealm(consumerPath))
304	uassert.PanicsContains(t, cur, "insufficient allowance", func() {
305		TransferFrom(0, cur, tokenKey, alice, bob, 100)
306	})
307	uassert.Equal(t, int64(1_000), MustGet(tokenKey).BalanceOf(alice))
308
309	// alice approves the consuming realm itself, and only then does it work.
310	urequire.NoError(t, ledger.Approve(alice, consumer, 100))
311
312	testing.SetRealm(testing.NewCodeRealm(consumerPath))
313	TransferFrom(0, cur, tokenKey, alice, bob, 100)
314	uassert.Equal(t, int64(900), MustGet(tokenKey).BalanceOf(alice))
315	uassert.Equal(t, int64(100), MustGet(tokenKey).BalanceOf(bob))
316}