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}