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}