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}