treasury_test.gno
12.68 Kb · 404 lines
1package test
2
3import (
4 "chain"
5 "chain/banker"
6 "chain/runtime/unsafe"
7 "sort"
8 "strings"
9 "testing"
10
11 "gno.land/p/nt/fqname/v0"
12 "gno.land/p/nt/grc20/v0"
13 "gno.land/p/nt/seqid/v0"
14 "gno.land/p/nt/testutils/v0"
15 trs_pkg "gno.land/p/nt/treasury/v0"
16 "gno.land/p/nt/uassert/v0"
17
18 "gno.land/r/gov/dao"
19 "gno.land/r/gov/dao/impl/v0"
20 "gno.land/r/gov/dao/treasury/v0"
21 "gno.land/r/nt/grc20reg/v0"
22)
23
24// cur is a zero-value realm used as a placeholder when forwarding to
25// uassert/urequire dispatch helpers that gained an `rlm realm` param.
26// These tests pass `func()` callbacks (no crossing inside the callback),
27// so rlm is ignored — a nil realm here is safe.
28var cur realm
29var nextTokenID seqid.ID
30var (
31 user1Addr = testutils.TestAddress("g1user1")
32 user2Addr = testutils.TestAddress("g1user2")
33 treasuryAddr = chain.PackageAddress("gno.land/r/gov/dao/treasury/v0")
34 allowedRealm = testing.NewCodeRealm("gno.land/r/test/allowed")
35 notAllowedRealm = testing.NewCodeRealm("gno.land/r/test/notallowed")
36 mintAmount = int64(1000)
37)
38
39// Define a dummy trs_pkg.Payment type for testing purposes.
40type dummyPayment struct {
41 bankerID string
42 str string
43}
44
45var _ trs_pkg.Payment = (*dummyPayment)(nil)
46
47func (dp *dummyPayment) BankerID() string { return dp.bankerID }
48func (dp *dummyPayment) String() string { return dp.str }
49
50func init(cur realm) {
51 // Register allowed Realm path.
52 dao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), []string{allowedRealm.PkgPath()}))
53}
54
55func ugnotCoins(t *testing.T, amount int64) chain.Coins {
56 t.Helper()
57
58 // Create a new coin with the ugnot denomination.
59 return chain.NewCoins(chain.NewCoin("ugnot", amount))
60}
61
62func ugnotBalance(t *testing.T, addr address) int64 {
63 t.Helper()
64
65 // Get the balance of ugnot coins for the given address.
66 banker_ := banker.NewReadonlyBanker()
67 coins := banker_.GetCoins(addr)
68
69 return coins.AmountOf("ugnot")
70}
71
72// Define a keyedToken type to hold the token and its key.
73type keyedToken struct {
74 key string
75 token *grc20.Token
76}
77
78func registerGRC20Tokens(_ int, rlm realm, t *testing.T, tokenNames []string, toMint address) []keyedToken {
79 t.Helper()
80
81 var (
82 keyedTokens = make([]keyedToken, 0, len(tokenNames))
83 keys = make([]string, 0, len(tokenNames))
84 )
85
86 for _, name := range tokenNames {
87 // Create the token.
88 symbol := strings.ToUpper(name)
89 token, ledger := grc20.NewToken(name, symbol, 0, nextTokenID.Next(), rlm)
90
91 // Register the token.
92 grc20reg.Register(cross(rlm), token, symbol)
93
94 // Mint tokens to the specified address.
95 ledger.Mint(toMint, mintAmount)
96
97 // Add the token and key to the lists.
98 key := fqname.Construct(unsafe.CurrentRealm().PkgPath(), symbol)
99 keyedTokens = append(keyedTokens, keyedToken{key: key, token: token})
100 keys = append(keys, key)
101 }
102
103 // Set the token keys in the treasury.
104 treasury.SetTokenKeys(cross(rlm), keys)
105
106 return keyedTokens
107}
108
109func TestAllowedDAOs(cur realm, t *testing.T) {
110 // Set the current Realm to the not allowed one.
111 testing.SetRealm(notAllowedRealm)
112
113 // Define a dummy payment to test sending.
114 dummyP := &dummyPayment{bankerID: "Dummy"}
115
116 // Try to send, it should abort because the Realm is not allowed.
117 uassert.AbortsWithMessage(
118 t, cur,
119 "this Realm is not allowed to send payment: "+notAllowedRealm.PkgPath(),
120 func() { treasury.Send(cross(cur), dummyP) },
121 )
122
123 // Set the current Realm to the allowed one.
124 testing.SetRealm(allowedRealm)
125
126 // Try to send, it should not abort because the Realm is allowed,
127 // but because the dummy banker ID is not registered.
128 uassert.AbortsWithMessage(
129 t, cur,
130 "banker not found: "+dummyP.BankerID(),
131 func() { treasury.Send(cross(cur), dummyP) },
132 )
133}
134
135func TestRegisteredBankers(t *testing.T) {
136 // Set the current Realm to the allowed one.
137 testing.SetRealm(allowedRealm)
138
139 // Define the expected banker IDs.
140 expectedBankerIDs := []string{
141 trs_pkg.CoinsBanker{}.ID(),
142 trs_pkg.GRC20Banker{}.ID(),
143 }
144
145 // Get the registered bankers from the treasury and compare their lengths.
146 registered := treasury.ListBankerIDs()
147 uassert.Equal(t, len(registered), len(expectedBankerIDs))
148
149 // The treasury-returned slice is foreign-readonly; copy locally
150 // before sorting in place.
151 registeredBankerIDs := append([]string(nil), registered...)
152
153 // Sort both slices then compare them.
154 sort.StringSlice(expectedBankerIDs).Sort()
155 sort.StringSlice(registeredBankerIDs).Sort()
156
157 for i := range expectedBankerIDs {
158 uassert.Equal(t, expectedBankerIDs[i], registeredBankerIDs[i])
159 }
160
161 // Test HasBanker method.
162 for _, bankerID := range expectedBankerIDs {
163 uassert.True(t, treasury.HasBanker(bankerID))
164 }
165 uassert.False(t, treasury.HasBanker("UnknownBankerID"))
166
167 // Test Address method.
168 for _, bankerID := range expectedBankerIDs {
169 // The two bankers used for now should have the treasury Realm address.
170 uassert.Equal(t, treasury.Address(bankerID), treasuryAddr.String())
171 }
172}
173
174func TestSendGRC20Payment(cur realm, t *testing.T) {
175 // Set the current Realm to the allowed one.
176 testing.SetRealm(allowedRealm)
177
178 // Try to send a GRC20 payment with a not registered token, it should abort.
179 uassert.AbortsWithMessage(
180 t, cur,
181 "failed to send payment: GRC20 token not found: UNKNOW",
182 func() {
183 treasury.Send(cross(cur), trs_pkg.NewGRC20Payment("UNKNOW", 100, user1Addr))
184 },
185 )
186
187 // Create 3 GRC20 tokens and register them.
188 keyedTokens := registerGRC20Tokens(
189 0, cur,
190 t,
191 []string{"TestToken0", "TestToken1", "TestToken2"},
192 treasuryAddr,
193 )
194
195 const txAmount = 42
196
197 // For each token-user pair.
198 for i, userAddr := range []address{user1Addr, user2Addr} {
199 for _, keyed := range keyedTokens {
200 // Check that the treasury has the expected balance before sending.
201 uassert.Equal(t, keyed.token.BalanceOf(treasuryAddr), mintAmount-int64(txAmount*i))
202
203 // Check that the user has no balance before sending.
204 uassert.Equal(t, keyed.token.BalanceOf(userAddr), int64(0))
205
206 // Try to send a GRC20 payment with a registered token, it should not abort.
207 uassert.NotAborts(t, cur, func() {
208 treasury.Send(
209 cross(cur),
210 trs_pkg.NewGRC20Payment(
211 keyed.key,
212 txAmount,
213 userAddr,
214 ),
215 )
216 })
217
218 // Check that the user has the expected balance after sending.
219 uassert.Equal(t, keyed.token.BalanceOf(userAddr), int64(txAmount))
220
221 // Check that the treasury has the expected balance after sending.
222 uassert.Equal(t, keyed.token.BalanceOf(treasuryAddr), mintAmount-int64(txAmount*(i+1)))
223 }
224 }
225
226 // Get the GRC20Banker ID.
227 grc20BankerID := trs_pkg.GRC20Banker{}.ID()
228
229 // Test Balances method for the GRC20Banker.
230 balances := treasury.Balances(grc20BankerID)
231 uassert.Equal(t, len(balances), len(keyedTokens))
232
233 compared := 0
234 for _, balance := range balances {
235 for _, keyed := range keyedTokens {
236 if balance.Denom == keyed.key {
237 uassert.Equal(t, balance.Amount, keyed.token.BalanceOf(treasuryAddr))
238 compared++
239 }
240 }
241 }
242 uassert.Equal(t, compared, len(keyedTokens))
243
244 // Check the history of the GRC20Banker.
245 history := treasury.History(grc20BankerID, 1, 10)
246 uassert.Equal(t, len(history), 6)
247
248 // Try to send a dummy payment with the GRC20 banker ID, it should abort.
249 uassert.AbortsWithMessage(
250 t, cur,
251 "failed to send payment: invalid payment type",
252 func() {
253 treasury.Send(cross(cur), &dummyPayment{bankerID: grc20BankerID})
254 },
255 )
256
257 // Try to send a GRC20 payment without enough balance, it should abort.
258 uassert.AbortsWithMessage(
259 t, cur,
260 "failed to send payment: insufficient balance",
261 func() {
262 treasury.Send(
263 cross(cur),
264 trs_pkg.NewGRC20Payment(
265 keyedTokens[0].key,
266 mintAmount*42, // Try to send more than the treasury has.
267 user1Addr,
268 ),
269 )
270 },
271 )
272
273 // Check the history of the GRC20Banker.
274 history = treasury.History(grc20BankerID, 1, 10)
275 uassert.Equal(t, len(history), 6)
276}
277
278func TestSendCoinPayment(cur realm, t *testing.T) {
279 // Set the current Realm to the allowed one.
280 testing.SetRealm(allowedRealm)
281
282 // Issue initial ugnot coins to the treasury address.
283 testing.IssueCoins(treasuryAddr, ugnotCoins(t, mintAmount))
284
285 // Get the CoinsBanker ID.
286 bankerID := trs_pkg.CoinsBanker{}.ID()
287
288 // Define helper function to check balances and history.
289 var (
290 expectedTreasuryBalance = mintAmount
291 expectedUser1Balance = int64(0)
292 expectedUser2Balance = int64(0)
293 expectedHistoryLen = 0
294 checkHistoryAndBalances = func() {
295 t.Helper()
296
297 uassert.Equal(t, ugnotBalance(t, treasuryAddr), expectedTreasuryBalance)
298 uassert.Equal(t, ugnotBalance(t, user1Addr), expectedUser1Balance)
299 uassert.Equal(t, ugnotBalance(t, user2Addr), expectedUser2Balance)
300
301 // Check treasury.Balances returned value.
302 balances := treasury.Balances(bankerID)
303 uassert.Equal(t, len(balances), 1)
304 uassert.Equal(t, balances[0].Denom, "ugnot")
305 uassert.Equal(t, balances[0].Amount, expectedTreasuryBalance)
306
307 // Check treasury.History returned value.
308 history := treasury.History(bankerID, 1, expectedHistoryLen+1)
309 uassert.Equal(t, len(history), expectedHistoryLen)
310 }
311 )
312
313 // Check initial balances and history.
314 checkHistoryAndBalances()
315
316 const txAmount = int64(42)
317
318 // Treasury send coins.
319 for i := int64(0); i < 3; i++ {
320 // Send ugnot coins to user1 and user2.
321 uassert.NotAborts(t, cur, func() {
322 treasury.Send(
323 cross(cur),
324 trs_pkg.NewCoinsPayment(ugnotCoins(t, txAmount), user1Addr),
325 )
326 treasury.Send(
327 cross(cur),
328 trs_pkg.NewCoinsPayment(ugnotCoins(t, txAmount), user2Addr),
329 )
330 })
331
332 // Update expected balances and history length.
333 expectedTreasuryBalance = mintAmount - txAmount*2*(i+1)
334 expectedUser1Balance = txAmount * (i + 1)
335 expectedUser2Balance = expectedUser1Balance
336 expectedHistoryLen = int(2 * (i + 1))
337
338 // Check balances and history after sending.
339 checkHistoryAndBalances()
340 }
341}
342
343// The allowlist is the only thing standing between an arbitrary realm and the
344// treasury: treasury.Send and treasury.SetTokenKeys gate on
345// dao.InAllowedDAOs, and that helper returns true for EVERY caller while the
346// list is empty (the genesis bootstrap window). UpdateImpl used to store an
347// empty list, so a single implementation-only upgrade — dao.UpdateImpl with
348// NewUpdateRequest(d, nil), which copies nil into a non-nil empty slice —
349// silently reopened that window and handed the treasury to the whole chain.
350//
351// TestAllowedDAOs above proves the gate rejects an outsider in the normal
352// state. This proves the gate cannot be switched off, which is the property
353// that actually protects the funds.
354func TestTreasuryLockdownCannotBeReopened(cur realm, t *testing.T) {
355 savedDAOs := dao.AllowedDAOs()
356
357 // An allowlisted realm attempts the reopen. NewUpdateRequest(d, nil) is the
358 // production spelling — it copies nil into a non-nil empty slice, which is
359 // exactly what the old `!= nil` test accepted. (A bare UpdateRequest struct
360 // literal cannot be built here: allocating another realm's struct type from
361 // this realm is rejected by the VM. That path is covered by
362 // r/gov/dao/allowlist_test.gno, which lives in the same package.)
363 testing.SetRealm(allowedRealm)
364 dao.UpdateImpl(cross(cur), dao.NewUpdateRequest(nil, nil))
365
366 // The outsider must still be refused at the treasury. Reaching
367 // "banker not found" would mean authorization had been cleared — that
368 // error comes from AFTER the InAllowedDAOs check.
369 testing.SetRealm(notAllowedRealm)
370 dummyP := &dummyPayment{bankerID: "Dummy"}
371
372 uassert.AbortsWithMessage(
373 t, cur,
374 "this Realm is not allowed to send payment: "+notAllowedRealm.PkgPath(),
375 func() { treasury.Send(cross(cur), dummyP) },
376 )
377
378 // SetTokenKeys reuses Send's message verbatim ("...to send payment...")
379 // rather than naming its own operation — a copy-paste slip in
380 // treasury.gno:65, asserted here as-is. Worth correcting separately; it is
381 // operator-visible text, not a security property, so it is not changed as
382 // part of this fix.
383 uassert.AbortsWithMessage(
384 t, cur,
385 "this Realm is not allowed to send payment: "+notAllowedRealm.PkgPath(),
386 func() { treasury.SetTokenKeys(cross(cur), []string{"evil/key"}) },
387 )
388
389 // Restore explicitly rather than via defer: the assertions above leave the
390 // current realm set to notAllowedRealm, and a deferred UpdateImpl would run
391 // under it and be refused. uassert.AbortsWithMessage recovers the abort, so
392 // control always reaches here.
393 testing.SetRealm(allowedRealm)
394 dao.UpdateImpl(cross(cur), dao.NewUpdateRequest(nil, savedDAOs))
395 uassert.True(t, dao.InAllowedDAOs(allowedRealm.PkgPath()), "allowlist restored")
396}
397
398func TestRenderEscapesUnknownBankerID(t *testing.T) {
399 // The {banker} route reflects an unknown banker id into the page; a crafted
400 // id must be escaped so it cannot inject markdown/HTML.
401 out := treasury.Render("evil[x](y)")
402 uassert.True(t, strings.Contains(out, `\[x\]`), "reflected banker id must be escaped")
403 uassert.False(t, strings.Contains(out, "[x](y)"), "must not render a live link")
404}