PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/nt/grc20reg/v0

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
v0
Namespace
nt / grc20reg
Files
3 (gnomod.toml)
Exported functions
8
Module
gno.land/r/nt/grc20reg/v0
gno
0.9

Files (3)

  • gnomod.tomltoml
  • grc20reg.gnogno
  • grc20reg_test.gnogno
grc20reg_test.gnogno
1package grc20reg23import (4	"chain"5	"strings"6	"testing"78	"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)1314func 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	// register19	key := Register(cross(cur), token, "mySlug")20	regToken := Get(key)21	urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil22	urequire.Equal(t, regToken.GetSymbol(), "TST")2324	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	// 40429	invalidToken := Get("0xdeadbeef")30	urequire.True(t, invalidToken == nil)3132	got = Render("")33	urequire.True(t, strings.Contains(got, expected))3435	expected = `# TestToken36- symbol: **TST**37- realm: [gno.land/r/demo/foo](/r/demo/foo).TST38- decimals: 439- total supply: 123456740`41	got = Render(key)42	urequire.Equal(t, expected, got)4344	// The registry keys by rlmPath.symbol, so a second token with the same45	// symbol in the same realm is rejected even though its Token.ID() differs46	// (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 symbol49	urequire.AbortsContains(t, cur, "token already registered", func() {50		Register(cross(cur), second, "")51	})52}5354// TestRegistryLookupGrantsNoSpendAuthority pins the property that replaced the55// former Transfer/Approve/TransferFrom wrappers: a registry lookup yields a56// *Token, and a *Token alone carries no authority to debit anybody. The57// frame-relative teller is reachable only from the ledger, which never leaves58// 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 one62// exercised below: the user grants an allowance, and the realm spends it as63// 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)7273	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))8081	// alice approves the consumer realm on the token. On chain she does this82	// 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))8586	// The consumer spends that allowance as ITSELF, over a token it found in87	// the registry and does not own. The actor is fixed at construction, so it88	// 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))9596	// 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}103104// TestRegistryConsumerCannotRedirectItsActor is the negative half of the105// property above: a realm holding a registered token pays out of its own106// balance, not the signing user's. The actor is bound at construction, so the107// transaction's origin caller has no bearing on who is debited — which is what108// 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)117118	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, "")123124	// alice signs the transaction, and holds a balance the consumer would love125	// 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}133134func 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, "")139140	urequire.Equal(t, "BAR", Get(key).GetSymbol())141	urequire.Equal(t, int64(11), Get(key).BalanceOf(cur.Address()))142143	replacement, _ := grc20.NewToken("Replacement", "BAR", 6, 0, cur)144	urequire.AbortsContains(t, cur, "token already registered", func() {145		Register(cross(cur), replacement, "")146	})147}148149func 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")153154	urequire.AbortsContains(t, cur, "token already registered", func() {155		Register(cross(cur), token, "second")156	})157}158159func 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)162163	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}168169func TestValidateSlug(cur realm, t *testing.T) {170	// Valid slugs — should not panic171	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 = pass174	}175}176177func TestValidateSlugPanicsOnTooLong(cur realm, t *testing.T) {178	defer func() { recover() }()179	validateSlug(strings.Repeat("a", maxSlugLen+1))180	t.Errorf("should have panicked")181}182183func TestValidateSlugPanicsOnSpace(cur realm, t *testing.T) {184	defer func() { recover() }()185	validateSlug("has space")186	t.Errorf("should have panicked")187}188189func TestValidateSlugPanicsOnDot(cur realm, t *testing.T) {190	defer func() { recover() }()191	validateSlug("has.dot")192	t.Errorf("should have panicked")193}194195func TestValidateSlugPanicsOnSlash(cur realm, t *testing.T) {196	defer func() { recover() }()197	validateSlug("has/slash")198	t.Errorf("should have panicked")199}200201func TestValidateSlugPanicsOnBrackets(cur realm, t *testing.T) {202	defer func() { recover() }()203	validateSlug("[brackets]")204	t.Errorf("should have panicked")205}206207func TestValidateSlugPanicsOnParens(cur realm, t *testing.T) {208	defer func() { recover() }()209	validateSlug("(parens)")210	t.Errorf("should have panicked")211}212213func TestValidateSlugPanicsOnInjection(cur realm, t *testing.T) {214	defer func() { recover() }()215	validateSlug(`) [Claim](https://evil.com`)216	t.Errorf("should have panicked")217}218219func 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}225226// TestWrappersBindActorToCallingRealm pins the property that makes the write227// wrappers safe: they are non-crossing, so `rlm` reaches RealmTeller as the228// caller's own live token and the debit lands on the caller. A crossing wrapper229// would mint a fresh `cur` for grc20reg and spend the registry's balance230// 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 is233// 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")242243	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, "")248249	testing.SetRealm(testing.NewCodeRealm(consumerPath))250	Transfer(0, cur, tokenKey, bob, 100)251252	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}256257// TestWrapperActorIgnoresSigningUser is the negative half: the signing user has258// a balance the calling realm would like to spend, and does not lose it. The259// actor comes from rlm.Address(), so the transaction's origin has no say in who260// 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)269270	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, "")275276	testing.SetOriginCaller(alice)277	testing.SetRealm(testing.NewCodeRealm(consumerPath))278	Transfer(0, cur, tokenKey, bob, 100)279280	uassert.Equal(t, int64(1_000), MustGet(tokenKey).BalanceOf(alice))281	uassert.Equal(t, int64(400), MustGet(tokenKey).BalanceOf(consumer))282}283284// TestTransferFromSpendsAllowanceGrantedToCallingRealm records the semantic285// shift that comes with binding the actor to the caller: the owner must have286// approved the CALLING REALM, not the signing user. An allowance granted to287// 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)296297	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, "")301302	// 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))308309	// alice approves the consuming realm itself, and only then does it work.310	urequire.NoError(t, ledger.Approve(alice, consumer, 100))311312	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}317

Functions

  • Approve(int, rlm interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, tokenKey string, spender string, amount int64)

  • Get(key string) *gno.land/p/nt/grc20/v0.Token

  • GetRegistry() *gno.land/p/nt/avl/rotree/v0.ReadOnlyTree

  • MustGet(key string) *gno.land/p/nt/grc20/v0.Token

  • Register(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, token *gno.land/p/nt/grc20/v0.Token, slug string) string

  • Render(path string) string

  • Transfer(int, rlm interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, tokenKey string, to string, amount int64)

  • TransferFrom(int, rlm interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, tokenKey string, from string, to string, amount int64)

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

Rendered

RenderedRawgnoweb ↗

vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.