1package test23import (4 "chain"5 "chain/banker"6 "chain/runtime/unsafe"7 "sort"8 "strings"9 "testing"1011 "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"1718 "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)2324// cur is a zero-value realm used as a placeholder when forwarding to25// 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 realm29var nextTokenID seqid.ID30var (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)3839// Define a dummy trs_pkg.Payment type for testing purposes.40type dummyPayment struct {41 bankerID string42 str string43}4445var _ trs_pkg.Payment = (*dummyPayment)(nil)4647func (dp *dummyPayment) BankerID() string { return dp.bankerID }48func (dp *dummyPayment) String() string { return dp.str }4950func init(cur realm) {51 // Register allowed Realm path.52 dao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), []string{allowedRealm.PkgPath()}))53}5455func ugnotCoins(t *testing.T, amount int64) chain.Coins {56 t.Helper()5758 // Create a new coin with the ugnot denomination.59 return chain.NewCoins(chain.NewCoin("ugnot", amount))60}6162func ugnotBalance(t *testing.T, addr address) int64 {63 t.Helper()6465 // Get the balance of ugnot coins for the given address.66 banker_ := banker.NewReadonlyBanker()67 coins := banker_.GetCoins(addr)6869 return coins.AmountOf("ugnot")70}7172// Define a keyedToken type to hold the token and its key.73type keyedToken struct {74 key string75 token *grc20.Token76}7778func registerGRC20Tokens(_ int, rlm realm, t *testing.T, tokenNames []string, toMint address) []keyedToken {79 t.Helper()8081 var (82 keyedTokens = make([]keyedToken, 0, len(tokenNames))83 keys = make([]string, 0, len(tokenNames))84 )8586 for _, name := range tokenNames {87 // Create the token.88 symbol := strings.ToUpper(name)89 token, ledger := grc20.NewToken(name, symbol, 0, nextTokenID.Next(), rlm)9091 // Register the token.92 grc20reg.Register(cross(rlm), token, symbol)9394 // Mint tokens to the specified address.95 ledger.Mint(toMint, mintAmount)9697 // 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 }102103 // Set the token keys in the treasury.104 treasury.SetTokenKeys(cross(rlm), keys)105106 return keyedTokens107}108109func TestAllowedDAOs(cur realm, t *testing.T) {110 // Set the current Realm to the not allowed one.111 testing.SetRealm(notAllowedRealm)112113 // Define a dummy payment to test sending.114 dummyP := &dummyPayment{bankerID: "Dummy"}115116 // 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 )122123 // Set the current Realm to the allowed one.124 testing.SetRealm(allowedRealm)125126 // 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}134135func TestRegisteredBankers(t *testing.T) {136 // Set the current Realm to the allowed one.137 testing.SetRealm(allowedRealm)138139 // Define the expected banker IDs.140 expectedBankerIDs := []string{141 trs_pkg.CoinsBanker{}.ID(),142 trs_pkg.GRC20Banker{}.ID(),143 }144145 // Get the registered bankers from the treasury and compare their lengths.146 registered := treasury.ListBankerIDs()147 uassert.Equal(t, len(registered), len(expectedBankerIDs))148149 // The treasury-returned slice is foreign-readonly; copy locally150 // before sorting in place.151 registeredBankerIDs := append([]string(nil), registered...)152153 // Sort both slices then compare them.154 sort.StringSlice(expectedBankerIDs).Sort()155 sort.StringSlice(registeredBankerIDs).Sort()156157 for i := range expectedBankerIDs {158 uassert.Equal(t, expectedBankerIDs[i], registeredBankerIDs[i])159 }160161 // Test HasBanker method.162 for _, bankerID := range expectedBankerIDs {163 uassert.True(t, treasury.HasBanker(bankerID))164 }165 uassert.False(t, treasury.HasBanker("UnknownBankerID"))166167 // 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}173174func TestSendGRC20Payment(cur realm, t *testing.T) {175 // Set the current Realm to the allowed one.176 testing.SetRealm(allowedRealm)177178 // 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 )186187 // Create 3 GRC20 tokens and register them.188 keyedTokens := registerGRC20Tokens(189 0, cur,190 t,191 []string{"TestToken0", "TestToken1", "TestToken2"},192 treasuryAddr,193 )194195 const txAmount = 42196197 // 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))202203 // Check that the user has no balance before sending.204 uassert.Equal(t, keyed.token.BalanceOf(userAddr), int64(0))205206 // 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 })217218 // Check that the user has the expected balance after sending.219 uassert.Equal(t, keyed.token.BalanceOf(userAddr), int64(txAmount))220221 // 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 }225226 // Get the GRC20Banker ID.227 grc20BankerID := trs_pkg.GRC20Banker{}.ID()228229 // Test Balances method for the GRC20Banker.230 balances := treasury.Balances(grc20BankerID)231 uassert.Equal(t, len(balances), len(keyedTokens))232233 compared := 0234 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))243244 // Check the history of the GRC20Banker.245 history := treasury.History(grc20BankerID, 1, 10)246 uassert.Equal(t, len(history), 6)247248 // 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 )256257 // 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 )272273 // Check the history of the GRC20Banker.274 history = treasury.History(grc20BankerID, 1, 10)275 uassert.Equal(t, len(history), 6)276}277278func TestSendCoinPayment(cur realm, t *testing.T) {279 // Set the current Realm to the allowed one.280 testing.SetRealm(allowedRealm)281282 // Issue initial ugnot coins to the treasury address.283 testing.IssueCoins(treasuryAddr, ugnotCoins(t, mintAmount))284285 // Get the CoinsBanker ID.286 bankerID := trs_pkg.CoinsBanker{}.ID()287288 // Define helper function to check balances and history.289 var (290 expectedTreasuryBalance = mintAmount291 expectedUser1Balance = int64(0)292 expectedUser2Balance = int64(0)293 expectedHistoryLen = 0294 checkHistoryAndBalances = func() {295 t.Helper()296297 uassert.Equal(t, ugnotBalance(t, treasuryAddr), expectedTreasuryBalance)298 uassert.Equal(t, ugnotBalance(t, user1Addr), expectedUser1Balance)299 uassert.Equal(t, ugnotBalance(t, user2Addr), expectedUser2Balance)300301 // 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)306307 // Check treasury.History returned value.308 history := treasury.History(bankerID, 1, expectedHistoryLen+1)309 uassert.Equal(t, len(history), expectedHistoryLen)310 }311 )312313 // Check initial balances and history.314 checkHistoryAndBalances()315316 const txAmount = int64(42)317318 // 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 })331332 // Update expected balances and history length.333 expectedTreasuryBalance = mintAmount - txAmount*2*(i+1)334 expectedUser1Balance = txAmount * (i + 1)335 expectedUser2Balance = expectedUser1Balance336 expectedHistoryLen = int(2 * (i + 1))337338 // Check balances and history after sending.339 checkHistoryAndBalances()340 }341}342343// The allowlist is the only thing standing between an arbitrary realm and the344// treasury: treasury.Send and treasury.SetTokenKeys gate on345// dao.InAllowedDAOs, and that helper returns true for EVERY caller while the346// list is empty (the genesis bootstrap window). UpdateImpl used to store an347// empty list, so a single implementation-only upgrade — dao.UpdateImpl with348// 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 normal352// state. This proves the gate cannot be switched off, which is the property353// that actually protects the funds.354func TestTreasuryLockdownCannotBeReopened(cur realm, t *testing.T) {355 savedDAOs := dao.AllowedDAOs()356357 // An allowlisted realm attempts the reopen. NewUpdateRequest(d, nil) is the358 // production spelling — it copies nil into a non-nil empty slice, which is359 // exactly what the old `!= nil` test accepted. (A bare UpdateRequest struct360 // literal cannot be built here: allocating another realm's struct type from361 // this realm is rejected by the VM. That path is covered by362 // 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))365366 // The outsider must still be refused at the treasury. Reaching367 // "banker not found" would mean authorization had been cleared — that368 // error comes from AFTER the InAllowedDAOs check.369 testing.SetRealm(notAllowedRealm)370 dummyP := &dummyPayment{bankerID: "Dummy"}371372 uassert.AbortsWithMessage(373 t, cur,374 "this Realm is not allowed to send payment: "+notAllowedRealm.PkgPath(),375 func() { treasury.Send(cross(cur), dummyP) },376 )377378 // SetTokenKeys reuses Send's message verbatim ("...to send payment...")379 // rather than naming its own operation — a copy-paste slip in380 // treasury.gno:65, asserted here as-is. Worth correcting separately; it is381 // operator-visible text, not a security property, so it is not changed as382 // 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 )388389 // Restore explicitly rather than via defer: the assertions above leave the390 // current realm set to notAllowedRealm, and a deferred UpdateImpl would run391 // under it and be refused. uassert.AbortsWithMessage recovers the abort, so392 // 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}397398func TestRenderEscapesUnknownBankerID(t *testing.T) {399 // The {banker} route reflects an unknown banker id into the page; a crafted400 // 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}405Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.