Search Apps Documentation Source Content File Folder Download Copy Actions Download

token_test.gno

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