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.05 Kb · 279 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	// transfer all tokens
206	admin.Transfer(owner, recipient, 100)
207	remainingBalance := token.BalanceOf(owner)
208	uassert.Equal(t, remainingBalance, int64(0),
209		"balance should be zero")
210
211	err = admin.TransferFrom(owner, spender, recipient, transferAmount)
212	uassert.Error(t, err, "transfer should fail due to insufficient balance")
213}
214
215func TestMintUntilOverflow(cur realm, t *testing.T) {
216	alice := testutils.TestAddress("alice")
217	bob := testutils.TestAddress("bob")
218	tok, adm := newTestToken("Dummy", "DUMMY", 6, 0, cur)
219
220	tests := []struct {
221		name           string
222		addr           address
223		amount         int64
224		expectedError  error
225		expectedSupply int64
226		description    string
227	}{
228		{
229			name:           "mint negative value",
230			addr:           alice,
231			amount:         -1,
232			expectedError:  ErrInvalidAmount,
233			expectedSupply: 0,
234			description:    "minting a negative number should fail with ErrInvalidAmount",
235		},
236		{
237			name:           "mint MaxInt64",
238			addr:           alice,
239			amount:         math.MaxInt64 - 1000,
240			expectedError:  nil,
241			expectedSupply: math.MaxInt64 - 1000,
242			description:    "minting almost MaxInt64 should succeed",
243		},
244		{
245			name:           "mint small value",
246			addr:           bob,
247			amount:         1000,
248			expectedError:  nil,
249			expectedSupply: math.MaxInt64,
250			description:    "minting a small value when close to MaxInt64 should succeed",
251		},
252		{
253			name:           "mint value that would exceed MaxInt64",
254			addr:           bob,
255			amount:         1,
256			expectedError:  ErrMintOverflow,
257			expectedSupply: math.MaxInt64,
258			description:    "minting any value when at MaxInt64 should fail with ErrMintOverflow",
259		},
260	}
261
262	for _, tt := range tests {
263		t.Run(tt.name, func(t *testing.T) {
264			err := adm.Mint(tt.addr, tt.amount)
265
266			if tt.expectedError != nil {
267				uassert.Error(t, err, tt.description)
268				if !errors.Is(err, tt.expectedError) {
269					t.Errorf("expected error %v, got %v", tt.expectedError, err)
270				}
271			} else {
272				uassert.NoError(t, err, tt.description)
273			}
274
275			totalSupply := tok.TotalSupply()
276			uassert.Equal(t, totalSupply, tt.expectedSupply, "totalSupply should match expected value")
277		})
278	}
279}