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

token_test.gno

9.57 Kb · 288 lines
  1package grc20
  2
  3import (
  4	"errors"
  5	"math"
  6	"strings"
  7	"testing"
  8
  9	"gno.land/p/nt/seqid/v0"
 10	"gno.land/p/nt/testutils/v0"
 11	"gno.land/p/nt/uassert/v0"
 12	"gno.land/p/nt/ufmt/v0"
 13	"gno.land/p/nt/urequire/v0"
 14)
 15
 16// newTestToken constructs a Token. The IIFE same-realm cross
 17// promotes the test's EOA-origin cur into a fresh /p/grc20
 18// CodeRealm cur so NewToken's IsCurrent + non-empty rlm.PkgPath()
 19// checks pass.
 20func newTestToken(name, symbol string, decimals int, id seqid.ID, rlm realm) (tok *Token, adm *PrivateLedger) {
 21	func(cur realm) {
 22		tok, adm = NewToken(name, symbol, decimals, id, cur)
 23	}(cross(rlm))
 24	return
 25}
 26
 27func TestTestImpl(cur realm, t *testing.T) {
 28	bank, _ := newTestToken("Dummy", "DUMMY", 4, 0, cur)
 29	urequire.False(t, bank == nil, "dummy should not be nil")
 30}
 31
 32func TestNewTokenAllowsDuplicateSymbolInSameRealm(cur realm, t *testing.T) {
 33	holder := testutils.TestAddress("holder")
 34
 35	first, firstLedger := newTestToken("Same", "DUP", 6, 1, cur)
 36	second, secondLedger := newTestToken("Same", "DUP", 6, 2, cur)
 37
 38	urequire.True(t, first.ID() != second.ID(), "duplicate symbols should not force duplicate IDs")
 39
 40	urequire.NoError(t, firstLedger.Mint(holder, 11))
 41	urequire.NoError(t, secondLedger.Mint(holder, 22))
 42	urequire.Equal(t, int64(11), first.BalanceOf(holder))
 43	urequire.Equal(t, int64(22), second.BalanceOf(holder))
 44}
 45
 46func TestNewTokenValidation(cur realm, t *testing.T) {
 47	// Wrap NewToken to satisfy IsCurrent and pkgPath requirements,
 48	// then validate name, symbol, and decimals.
 49	mustPanic := func(name, sym string, dec int, want error, label string) {
 50		t.Helper()
 51		// newTestToken wraps NewToken in cross(...), so the panic from
 52		// NewToken's validators crosses a realm boundary — use revive()
 53		// (defer-recover doesn't see cross-realm panics).
 54		r := revive(func() {
 55			newTestToken(name, sym, dec, 0, cur)
 56		})
 57		if r == nil {
 58			t.Errorf("%s: expected panic, got none", label)
 59			return
 60		}
 61		if r != want {
 62			t.Errorf("%s: expected %v, got %v", label, want, r)
 63		}
 64	}
 65
 66	// Empty name / symbol.
 67	mustPanic("", "OK", 4, ErrInvalidName, "empty name")
 68	mustPanic("Name", "", 4, ErrInvalidSymbol, "empty symbol")
 69
 70	// Length caps.
 71	mustPanic(strings.Repeat("a", MaxNameLen+1), "OK", 4, ErrInvalidName, "name too long")
 72	mustPanic("Name", strings.Repeat("A", MaxSymbolLen+1), 4, ErrInvalidSymbol, "symbol too long")
 73
 74	// Name control characters.
 75	mustPanic("bad\x01name", "OK", 4, ErrInvalidName, "name with control char")
 76	mustPanic("bad\nname", "OK", 4, ErrInvalidName, "name with newline")
 77
 78	// Symbol charset — disallowed delimiters and whitespace.
 79	mustPanic("Name", "BA.D", 4, ErrInvalidSymbol, "symbol with dot")
 80	mustPanic("Name", "BA/D", 4, ErrInvalidSymbol, "symbol with slash")
 81	mustPanic("Name", "BA D", 4, ErrInvalidSymbol, "symbol with space")
 82	mustPanic("Name", "BA\"D", 4, ErrInvalidSymbol, "symbol with quote")
 83
 84	// Decimals out of range.
 85	mustPanic("Name", "OK", -1, ErrInvalidDecimals, "negative decimals")
 86	mustPanic("Name", "OK", MaxDecimals+1, ErrInvalidDecimals, "decimals over cap")
 87
 88	// Boundary positives — should NOT panic.
 89	tok, _ := newTestToken(strings.Repeat("a", MaxNameLen), strings.Repeat("A", MaxSymbolLen), MaxDecimals, 0, cur)
 90	urequire.True(t, tok != nil, "boundary name+symbol+decimals should succeed")
 91
 92	// UTF-8 name with non-ASCII is allowed.
 93	tok2, _ := newTestToken("Доллар", "RUB", 2, 0, cur)
 94	urequire.True(t, tok2 != nil, "UTF-8 name should be allowed")
 95}
 96
 97func TestToken(cur realm, t *testing.T) {
 98	var (
 99		alice = testutils.TestAddress("alice")
100		bob   = testutils.TestAddress("bob")
101		carl  = testutils.TestAddress("carl")
102	)
103
104	bank, adm := newTestToken("Dummy", "DUMMY", 6, 0, cur)
105
106	checkBalances := func(aliceEB, bobEB, carlEB int64) {
107		t.Helper()
108		exp := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceEB, bobEB, carlEB)
109		aliceGB := bank.BalanceOf(alice)
110		bobGB := bank.BalanceOf(bob)
111		carlGB := bank.BalanceOf(carl)
112		got := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceGB, bobGB, carlGB)
113		uassert.Equal(t, got, exp, "invalid balances")
114	}
115	checkAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB int64) {
116		t.Helper()
117		exp := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abEB, acEB, baEB, bcEB, caEB, cbEB)
118		abGB := bank.Allowance(alice, bob)
119		acGB := bank.Allowance(alice, carl)
120		baGB := bank.Allowance(bob, alice)
121		bcGB := bank.Allowance(bob, carl)
122		caGB := bank.Allowance(carl, alice)
123		cbGB := bank.Allowance(carl, bob)
124		got := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abGB, acGB, baGB, bcGB, caGB, cbGB)
125		uassert.Equal(t, got, exp, "invalid allowances")
126	}
127
128	checkBalances(0, 0, 0)
129	checkAllowances(0, 0, 0, 0, 0, 0)
130
131	urequire.NoError(t, adm.Mint(alice, 1000))
132	urequire.NoError(t, adm.Mint(alice, 100))
133	checkBalances(1100, 0, 0)
134	checkAllowances(0, 0, 0, 0, 0, 0)
135
136	urequire.NoError(t, adm.Approve(alice, bob, 99999999))
137	checkBalances(1100, 0, 0)
138	checkAllowances(99999999, 0, 0, 0, 0, 0)
139
140	urequire.NoError(t, adm.Approve(alice, bob, 400))
141	checkBalances(1100, 0, 0)
142	checkAllowances(400, 0, 0, 0, 0, 0)
143
144	urequire.Error(t, adm.TransferFrom(alice, bob, carl, 100000000))
145	checkBalances(1100, 0, 0)
146	checkAllowances(400, 0, 0, 0, 0, 0)
147
148	urequire.NoError(t, adm.TransferFrom(alice, bob, carl, 100))
149	checkBalances(1000, 0, 100)
150	checkAllowances(300, 0, 0, 0, 0, 0)
151
152	urequire.Error(t, adm.SpendAllowance(alice, bob, 2000000))
153	checkBalances(1000, 0, 100)
154	checkAllowances(300, 0, 0, 0, 0, 0)
155
156	urequire.NoError(t, adm.SpendAllowance(alice, bob, 100))
157	checkBalances(1000, 0, 100)
158	checkAllowances(200, 0, 0, 0, 0, 0)
159}
160
161func TestMintOverflow(cur realm, t *testing.T) {
162	alice := testutils.TestAddress("alice")
163	bob := testutils.TestAddress("bob")
164	tok, adm := newTestToken("Dummy", "DUMMY", 6, 0, cur)
165
166	safeValue := int64(1 << 62)
167	urequire.NoError(t, adm.Mint(alice, safeValue))
168	urequire.Equal(t, tok.BalanceOf(alice), safeValue)
169
170	err := adm.Mint(bob, safeValue)
171	uassert.Error(t, err, "expected ErrMintOverflow")
172}
173
174func TestTransferFromAtomicity(cur realm, t *testing.T) {
175	var (
176		owner   = testutils.TestAddress("owner")
177		spender = testutils.TestAddress("spender")
178
179		invalidRecipient = address("")
180		recipient        = testutils.TestAddress("to")
181	)
182
183	token, admin := newTestToken("Test", "TEST", 6, 0, cur)
184
185	// owner has 100 tokens, spender has 50 allowance
186	initialBalance := int64(100)
187	initialAllowance := int64(50)
188
189	urequire.NoError(t, admin.Mint(owner, initialBalance))
190	urequire.NoError(t, admin.Approve(owner, spender, initialAllowance))
191
192	// transfer to an invalid address to force a transfer failure
193	transferAmount := int64(30)
194	err := admin.TransferFrom(owner, spender, invalidRecipient, transferAmount)
195	uassert.Error(t, err, "transfer should fail due to invalid address")
196
197	ownerBalance := token.BalanceOf(owner)
198	uassert.Equal(t, ownerBalance, initialBalance, "owner balance should remain unchanged")
199
200	// check if allowance was incorrectly reduced
201	remainingAllowance := token.Allowance(owner, spender)
202	uassert.Equal(t, remainingAllowance, initialAllowance,
203		"allowance should not be reduced when transfer fails")
204
205	// Self-transfers fail in Transfer after SpendAllowance. TransferFrom must
206	// reject them before it can mutate the allowance.
207	err = admin.TransferFrom(owner, spender, owner, transferAmount)
208	uassert.True(t, errors.Is(err, ErrCannotTransferToSelf), "self-transfer should fail")
209	uassert.Equal(t, initialBalance, token.BalanceOf(owner),
210		"owner balance should not change when self-transfer fails")
211	uassert.Equal(t, initialAllowance, token.Allowance(owner, spender),
212		"allowance should not be reduced when self-transfer fails")
213
214	// transfer all tokens
215	admin.Transfer(owner, recipient, 100)
216	remainingBalance := token.BalanceOf(owner)
217	uassert.Equal(t, remainingBalance, int64(0),
218		"balance should be zero")
219
220	err = admin.TransferFrom(owner, spender, recipient, transferAmount)
221	uassert.Error(t, err, "transfer should fail due to insufficient balance")
222}
223
224func TestMintUntilOverflow(cur realm, t *testing.T) {
225	alice := testutils.TestAddress("alice")
226	bob := testutils.TestAddress("bob")
227	tok, adm := newTestToken("Dummy", "DUMMY", 6, 0, cur)
228
229	tests := []struct {
230		name           string
231		addr           address
232		amount         int64
233		expectedError  error
234		expectedSupply int64
235		description    string
236	}{
237		{
238			name:           "mint negative value",
239			addr:           alice,
240			amount:         -1,
241			expectedError:  ErrInvalidAmount,
242			expectedSupply: 0,
243			description:    "minting a negative number should fail with ErrInvalidAmount",
244		},
245		{
246			name:           "mint MaxInt64",
247			addr:           alice,
248			amount:         math.MaxInt64 - 1000,
249			expectedError:  nil,
250			expectedSupply: math.MaxInt64 - 1000,
251			description:    "minting almost MaxInt64 should succeed",
252		},
253		{
254			name:           "mint small value",
255			addr:           bob,
256			amount:         1000,
257			expectedError:  nil,
258			expectedSupply: math.MaxInt64,
259			description:    "minting a small value when close to MaxInt64 should succeed",
260		},
261		{
262			name:           "mint value that would exceed MaxInt64",
263			addr:           bob,
264			amount:         1,
265			expectedError:  ErrMintOverflow,
266			expectedSupply: math.MaxInt64,
267			description:    "minting any value when at MaxInt64 should fail with ErrMintOverflow",
268		},
269	}
270
271	for _, tt := range tests {
272		t.Run(tt.name, func(t *testing.T) {
273			err := adm.Mint(tt.addr, tt.amount)
274
275			if tt.expectedError != nil {
276				uassert.Error(t, err, tt.description)
277				if !errors.Is(err, tt.expectedError) {
278					t.Errorf("expected error %v, got %v", tt.expectedError, err)
279				}
280			} else {
281				uassert.NoError(t, err, tt.description)
282			}
283
284			totalSupply := tok.TotalSupply()
285			uassert.Equal(t, totalSupply, tt.expectedSupply, "totalSupply should match expected value")
286		})
287	}
288}