package test import ( "chain" "chain/banker" "chain/runtime/unsafe" "sort" "strings" "testing" "gno.land/p/nt/fqname/v0" "gno.land/p/nt/grc20/v0" "gno.land/p/nt/seqid/v0" "gno.land/p/nt/testutils/v0" trs_pkg "gno.land/p/nt/treasury/v0" "gno.land/p/nt/uassert/v0" "gno.land/r/gov/dao" "gno.land/r/gov/dao/impl/v0" "gno.land/r/gov/dao/treasury/v0" "gno.land/r/nt/grc20reg/v0" ) // cur is a zero-value realm used as a placeholder when forwarding to // uassert/urequire dispatch helpers that gained an `rlm realm` param. // These tests pass `func()` callbacks (no crossing inside the callback), // so rlm is ignored — a nil realm here is safe. var cur realm var nextTokenID seqid.ID var ( user1Addr = testutils.TestAddress("g1user1") user2Addr = testutils.TestAddress("g1user2") treasuryAddr = chain.PackageAddress("gno.land/r/gov/dao/treasury/v0") allowedRealm = testing.NewCodeRealm("gno.land/r/test/allowed") notAllowedRealm = testing.NewCodeRealm("gno.land/r/test/notallowed") mintAmount = int64(1000) ) // Define a dummy trs_pkg.Payment type for testing purposes. type dummyPayment struct { bankerID string str string } var _ trs_pkg.Payment = (*dummyPayment)(nil) func (dp *dummyPayment) BankerID() string { return dp.bankerID } func (dp *dummyPayment) String() string { return dp.str } func init(cur realm) { // Register allowed Realm path. dao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), []string{allowedRealm.PkgPath()})) } func ugnotCoins(t *testing.T, amount int64) chain.Coins { t.Helper() // Create a new coin with the ugnot denomination. return chain.NewCoins(chain.NewCoin("ugnot", amount)) } func ugnotBalance(t *testing.T, addr address) int64 { t.Helper() // Get the balance of ugnot coins for the given address. banker_ := banker.NewReadonlyBanker() coins := banker_.GetCoins(addr) return coins.AmountOf("ugnot") } // Define a keyedToken type to hold the token and its key. type keyedToken struct { key string token *grc20.Token } func registerGRC20Tokens(_ int, rlm realm, t *testing.T, tokenNames []string, toMint address) []keyedToken { t.Helper() var ( keyedTokens = make([]keyedToken, 0, len(tokenNames)) keys = make([]string, 0, len(tokenNames)) ) for _, name := range tokenNames { // Create the token. symbol := strings.ToUpper(name) token, ledger := grc20.NewToken(name, symbol, 0, nextTokenID.Next(), rlm) // Register the token. grc20reg.Register(cross(rlm), token, symbol) // Mint tokens to the specified address. ledger.Mint(toMint, mintAmount) // Add the token and key to the lists. key := fqname.Construct(unsafe.CurrentRealm().PkgPath(), symbol) keyedTokens = append(keyedTokens, keyedToken{key: key, token: token}) keys = append(keys, key) } // Set the token keys in the treasury. treasury.SetTokenKeys(cross(rlm), keys) return keyedTokens } func TestAllowedDAOs(cur realm, t *testing.T) { // Set the current Realm to the not allowed one. testing.SetRealm(notAllowedRealm) // Define a dummy payment to test sending. dummyP := &dummyPayment{bankerID: "Dummy"} // Try to send, it should abort because the Realm is not allowed. uassert.AbortsWithMessage( t, cur, "this Realm is not allowed to send payment: "+notAllowedRealm.PkgPath(), func() { treasury.Send(cross(cur), dummyP) }, ) // Set the current Realm to the allowed one. testing.SetRealm(allowedRealm) // Try to send, it should not abort because the Realm is allowed, // but because the dummy banker ID is not registered. uassert.AbortsWithMessage( t, cur, "banker not found: "+dummyP.BankerID(), func() { treasury.Send(cross(cur), dummyP) }, ) } func TestRegisteredBankers(t *testing.T) { // Set the current Realm to the allowed one. testing.SetRealm(allowedRealm) // Define the expected banker IDs. expectedBankerIDs := []string{ trs_pkg.CoinsBanker{}.ID(), trs_pkg.GRC20Banker{}.ID(), } // Get the registered bankers from the treasury and compare their lengths. registered := treasury.ListBankerIDs() uassert.Equal(t, len(registered), len(expectedBankerIDs)) // The treasury-returned slice is foreign-readonly; copy locally // before sorting in place. registeredBankerIDs := append([]string(nil), registered...) // Sort both slices then compare them. sort.StringSlice(expectedBankerIDs).Sort() sort.StringSlice(registeredBankerIDs).Sort() for i := range expectedBankerIDs { uassert.Equal(t, expectedBankerIDs[i], registeredBankerIDs[i]) } // Test HasBanker method. for _, bankerID := range expectedBankerIDs { uassert.True(t, treasury.HasBanker(bankerID)) } uassert.False(t, treasury.HasBanker("UnknownBankerID")) // Test Address method. for _, bankerID := range expectedBankerIDs { // The two bankers used for now should have the treasury Realm address. uassert.Equal(t, treasury.Address(bankerID), treasuryAddr.String()) } } func TestSendGRC20Payment(cur realm, t *testing.T) { // Set the current Realm to the allowed one. testing.SetRealm(allowedRealm) // Try to send a GRC20 payment with a not registered token, it should abort. uassert.AbortsWithMessage( t, cur, "failed to send payment: GRC20 token not found: UNKNOW", func() { treasury.Send(cross(cur), trs_pkg.NewGRC20Payment("UNKNOW", 100, user1Addr)) }, ) // Create 3 GRC20 tokens and register them. keyedTokens := registerGRC20Tokens( 0, cur, t, []string{"TestToken0", "TestToken1", "TestToken2"}, treasuryAddr, ) const txAmount = 42 // For each token-user pair. for i, userAddr := range []address{user1Addr, user2Addr} { for _, keyed := range keyedTokens { // Check that the treasury has the expected balance before sending. uassert.Equal(t, keyed.token.BalanceOf(treasuryAddr), mintAmount-int64(txAmount*i)) // Check that the user has no balance before sending. uassert.Equal(t, keyed.token.BalanceOf(userAddr), int64(0)) // Try to send a GRC20 payment with a registered token, it should not abort. uassert.NotAborts(t, cur, func() { treasury.Send( cross(cur), trs_pkg.NewGRC20Payment( keyed.key, txAmount, userAddr, ), ) }) // Check that the user has the expected balance after sending. uassert.Equal(t, keyed.token.BalanceOf(userAddr), int64(txAmount)) // Check that the treasury has the expected balance after sending. uassert.Equal(t, keyed.token.BalanceOf(treasuryAddr), mintAmount-int64(txAmount*(i+1))) } } // Get the GRC20Banker ID. grc20BankerID := trs_pkg.GRC20Banker{}.ID() // Test Balances method for the GRC20Banker. balances := treasury.Balances(grc20BankerID) uassert.Equal(t, len(balances), len(keyedTokens)) compared := 0 for _, balance := range balances { for _, keyed := range keyedTokens { if balance.Denom == keyed.key { uassert.Equal(t, balance.Amount, keyed.token.BalanceOf(treasuryAddr)) compared++ } } } uassert.Equal(t, compared, len(keyedTokens)) // Check the history of the GRC20Banker. history := treasury.History(grc20BankerID, 1, 10) uassert.Equal(t, len(history), 6) // Try to send a dummy payment with the GRC20 banker ID, it should abort. uassert.AbortsWithMessage( t, cur, "failed to send payment: invalid payment type", func() { treasury.Send(cross(cur), &dummyPayment{bankerID: grc20BankerID}) }, ) // Try to send a GRC20 payment without enough balance, it should abort. uassert.AbortsWithMessage( t, cur, "failed to send payment: insufficient balance", func() { treasury.Send( cross(cur), trs_pkg.NewGRC20Payment( keyedTokens[0].key, mintAmount*42, // Try to send more than the treasury has. user1Addr, ), ) }, ) // Check the history of the GRC20Banker. history = treasury.History(grc20BankerID, 1, 10) uassert.Equal(t, len(history), 6) } func TestSendCoinPayment(cur realm, t *testing.T) { // Set the current Realm to the allowed one. testing.SetRealm(allowedRealm) // Issue initial ugnot coins to the treasury address. testing.IssueCoins(treasuryAddr, ugnotCoins(t, mintAmount)) // Get the CoinsBanker ID. bankerID := trs_pkg.CoinsBanker{}.ID() // Define helper function to check balances and history. var ( expectedTreasuryBalance = mintAmount expectedUser1Balance = int64(0) expectedUser2Balance = int64(0) expectedHistoryLen = 0 checkHistoryAndBalances = func() { t.Helper() uassert.Equal(t, ugnotBalance(t, treasuryAddr), expectedTreasuryBalance) uassert.Equal(t, ugnotBalance(t, user1Addr), expectedUser1Balance) uassert.Equal(t, ugnotBalance(t, user2Addr), expectedUser2Balance) // Check treasury.Balances returned value. balances := treasury.Balances(bankerID) uassert.Equal(t, len(balances), 1) uassert.Equal(t, balances[0].Denom, "ugnot") uassert.Equal(t, balances[0].Amount, expectedTreasuryBalance) // Check treasury.History returned value. history := treasury.History(bankerID, 1, expectedHistoryLen+1) uassert.Equal(t, len(history), expectedHistoryLen) } ) // Check initial balances and history. checkHistoryAndBalances() const txAmount = int64(42) // Treasury send coins. for i := int64(0); i < 3; i++ { // Send ugnot coins to user1 and user2. uassert.NotAborts(t, cur, func() { treasury.Send( cross(cur), trs_pkg.NewCoinsPayment(ugnotCoins(t, txAmount), user1Addr), ) treasury.Send( cross(cur), trs_pkg.NewCoinsPayment(ugnotCoins(t, txAmount), user2Addr), ) }) // Update expected balances and history length. expectedTreasuryBalance = mintAmount - txAmount*2*(i+1) expectedUser1Balance = txAmount * (i + 1) expectedUser2Balance = expectedUser1Balance expectedHistoryLen = int(2 * (i + 1)) // Check balances and history after sending. checkHistoryAndBalances() } } // The allowlist is the only thing standing between an arbitrary realm and the // treasury: treasury.Send and treasury.SetTokenKeys gate on // dao.InAllowedDAOs, and that helper returns true for EVERY caller while the // list is empty (the genesis bootstrap window). UpdateImpl used to store an // empty list, so a single implementation-only upgrade — dao.UpdateImpl with // NewUpdateRequest(d, nil), which copies nil into a non-nil empty slice — // silently reopened that window and handed the treasury to the whole chain. // // TestAllowedDAOs above proves the gate rejects an outsider in the normal // state. This proves the gate cannot be switched off, which is the property // that actually protects the funds. func TestTreasuryLockdownCannotBeReopened(cur realm, t *testing.T) { savedDAOs := dao.AllowedDAOs() // An allowlisted realm attempts the reopen. NewUpdateRequest(d, nil) is the // production spelling — it copies nil into a non-nil empty slice, which is // exactly what the old `!= nil` test accepted. (A bare UpdateRequest struct // literal cannot be built here: allocating another realm's struct type from // this realm is rejected by the VM. That path is covered by // r/gov/dao/allowlist_test.gno, which lives in the same package.) testing.SetRealm(allowedRealm) dao.UpdateImpl(cross(cur), dao.NewUpdateRequest(nil, nil)) // The outsider must still be refused at the treasury. Reaching // "banker not found" would mean authorization had been cleared — that // error comes from AFTER the InAllowedDAOs check. testing.SetRealm(notAllowedRealm) dummyP := &dummyPayment{bankerID: "Dummy"} uassert.AbortsWithMessage( t, cur, "this Realm is not allowed to send payment: "+notAllowedRealm.PkgPath(), func() { treasury.Send(cross(cur), dummyP) }, ) // SetTokenKeys reuses Send's message verbatim ("...to send payment...") // rather than naming its own operation — a copy-paste slip in // treasury.gno:65, asserted here as-is. Worth correcting separately; it is // operator-visible text, not a security property, so it is not changed as // part of this fix. uassert.AbortsWithMessage( t, cur, "this Realm is not allowed to send payment: "+notAllowedRealm.PkgPath(), func() { treasury.SetTokenKeys(cross(cur), []string{"evil/key"}) }, ) // Restore explicitly rather than via defer: the assertions above leave the // current realm set to notAllowedRealm, and a deferred UpdateImpl would run // under it and be refused. uassert.AbortsWithMessage recovers the abort, so // control always reaches here. testing.SetRealm(allowedRealm) dao.UpdateImpl(cross(cur), dao.NewUpdateRequest(nil, savedDAOs)) uassert.True(t, dao.InAllowedDAOs(allowedRealm.PkgPath()), "allowlist restored") } func TestRenderEscapesUnknownBankerID(t *testing.T) { // The {banker} route reflects an unknown banker id into the page; a crafted // id must be escaped so it cannot inject markdown/HTML. out := treasury.Render("evil[x](y)") uassert.True(t, strings.Contains(out, `\[x\]`), "reflected banker id must be escaped") uassert.False(t, strings.Contains(out, "[x](y)"), "must not render a live link") }