Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

exploit_test.gno

10.32 Kb · 252 lines
  1package game32
  2
  3// The 12 game27/game28 exploit PoCs (SECURITY_GAME28.md), ported to game29.
  4// Convention here is the reverse of the audit copy: a test PASSES when the
  5// exploit is BLOCKED.
  6
  7import (
  8	"chain"
  9	"strconv"
 10	"strings"
 11	"testing"
 12)
 13
 14const forgedSig = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
 15
 16// H1a: forged dust bank with zero clicks + free Gnome Pass.
 17func TestH1a_ForgeDustBankWithFreePass(cur realm, t *testing.T) {
 18	setup()
 19	bot := ta("h1a-bot")
 20	testing.SetRealm(testing.NewUserRealm(bot))
 21	mustAbort(t, "free pass", revive(func() { ActivateGnomePass(cross(cur), 3650) }), "")
 22	mustAbort(t, "free pass 30d", revive(func() { ActivateGnomePass(cross(cur), 30) }), "attach exactly")
 23	if HasActivePass(bot.String()) {
 24		t.Errorf("pass still free")
 25	}
 26	// No server voucher → no claim, whatever the signature.
 27	mustAbort(t, "unvouchered claim", revive(func() {
 28		ClaimAndMint(cross(cur), `[["grin","1440"]]`, 1, 1234567890+600, forgedSig)
 29	}), "invalid voucher signature")
 30	if bal(bot, "grin") != 0 {
 31		t.Errorf("bot forged %d grin", bal(bot, "grin"))
 32	}
 33}
 34
 35// H1b: dust refilling forever for a bot. A voucher is single use, so the bot
 36// can't reuse one after the bucket refills.
 37func TestH1b_DustRefillsForever(cur realm, t *testing.T) {
 38	setup()
 39	bot := ta("h1b-bot")
 40	payload := `[["grin","100"]]`
 41	n, e, s := voucher(t, "claim", bot, payload)
 42	testing.SetRealm(testing.NewUserRealm(bot))
 43	ClaimAndMint(cross(cur), payload, n, e, s) // one legit, vouchered claim
 44	testing.SkipHeights(720)                   // 1 h later the bucket is full again…
 45	mustAbort(t, "replay after refill", revive(func() { ClaimAndMint(cross(cur), payload, n, e, s) }), "")
 46	if bal(bot, "grin") != 100 {
 47		t.Errorf("grin=%d, want 100", bal(bot, "grin"))
 48	}
 49}
 50
 51// H1c: made-up coin ids, even with a valid voucher.
 52func TestH1c_FakeCoinId(cur realm, t *testing.T) {
 53	setup()
 54	bot := ta("h1c-bot")
 55	payload := `[["fake-coin","50000"]]`
 56	n, e, s := voucher(t, "claim", bot, payload)
 57	testing.SetRealm(testing.NewUserRealm(bot))
 58	mustAbort(t, "fake coin", revive(func() { ClaimAndMint(cross(cur), payload, n, e, s) }), "unknown coin id")
 59}
 60
 61// H1d: Fist mining mythic coins, even with a valid voucher.
 62func TestH1d_FistForgesMythic(cur realm, t *testing.T) {
 63	setup()
 64	bot := ta("h1d-bot")
 65	payload := `[["dragon","1"]]`
 66	n, e, s := voucher(t, "claim", bot, payload)
 67	testing.SetRealm(testing.NewUserRealm(bot))
 68	mustAbort(t, "fist→mythic", revive(func() { ClaimAndMint(cross(cur), payload, n, e, s) }), "cannot mine rarity")
 69	if bal(bot, "dragon") != 0 {
 70		t.Errorf("dragon minted")
 71	}
 72}
 73
 74// H1e: zero-click tool climb. Without vouchers a script claims nothing, and a
 75// fresh bucket only holds 5 min of allowance (game28: a full hour).
 76func TestH1e_ZeroClickToolClimb(cur realm, t *testing.T) {
 77	setup()
 78	bot := ta("h1e-bot")
 79	testing.SetRealm(testing.NewUserRealm(bot))
 80	for i := 0; i < 5; i++ {
 81		_ = revive(func() { ClaimAndMint(cross(cur), `[["grin","24"]]`, int64(i+1), 1234567890+600, forgedSig) })
 82		testing.SkipHeights(13)
 83	}
 84	if bal(bot, "grin") != 0 {
 85		t.Errorf("zero-click bot got %d grin", bal(bot, "grin"))
 86	}
 87	mustAbort(t, "buy starter-twig", revive(func() { BuyTool(cross(cur), "starter-twig") }), "insufficient")
 88	fresh := ta("h1e-fresh")
 89	if a := allowanceFor(fresh, 0); a != 210 {
 90		t.Errorf("fresh dust allowance = %d, want 210 (5 min @ 42/min, year-plan dust headroom 1.75)", a)
 91	}
 92	// Even a vouchered claim can't exceed the 5-min grace bank.
 93	payload := `[["grin","1440"]]`
 94	n, e, s := voucher(t, "claim", fresh, payload)
 95	testing.SetRealm(testing.NewUserRealm(fresh))
 96	mustAbort(t, "1h bank on fresh wallet", revive(func() { ClaimAndMint(cross(cur), payload, n, e, s) }), "rate limit")
 97	if bal(fresh, "grin") != 0 {
 98		t.Errorf("fresh wallet got %d", bal(fresh, "grin"))
 99	}
100}
101
102// H1f: sybil funnel. Sybils can't mint without vouchers, so there's nothing
103// to funnel; a voucher for one wallet doesn't work for another.
104func TestH1f_SybilFunnel(cur realm, t *testing.T) {
105	setup()
106	main := ta("h1f-main")
107	payload := `[["grin","100"]]`
108	n, e, s := voucher(t, "claim", main, payload) // a real voucher for main
109	for i := 0; i < 10; i++ {
110		sy := ta("h1f-sybil-" + strconv.Itoa(i))
111		testing.SetRealm(testing.NewUserRealm(sy))
112		_ = revive(func() { ActivateGnomePass(cross(cur), 3650) })
113		_ = revive(func() { ClaimAndMint(cross(cur), payload, n, e, s) }) // stolen voucher
114		_ = revive(func() { Transfer(cross(cur), "grin", main.String(), "100") })
115		if bal(sy, "grin") != 0 {
116			t.Errorf("sybil %d minted", i)
117		}
118	}
119	if bal(main, "grin") != 0 {
120		t.Errorf("main received %d grin from sybils", bal(main, "grin"))
121	}
122}
123
124// H2a: PvP reveal never checked against the commit.
125func TestH2a_PvpRevealIgnoresCommit(cur realm, t *testing.T) {
126	setup()
127	alice, bob := ta("h2a-alice"), ta("h2a-bob")
128	give(alice, "grin", 1000)
129	give(bob, "grin", 1000)
130	testing.SetRealm(testing.NewUserRealm(alice))
131	lobby := CreatePvpLobby(cross(cur), "rps", "grin", "0")
132	testing.SetRealm(testing.NewUserRealm(bob))
133	JoinPvpLobby(cross(cur), lobby)
134	testing.SetRealm(testing.NewUserRealm(alice))
135	CommitPvpMove(cross(cur), lobby, h("rock", "a-salt"))
136	testing.SetRealm(testing.NewUserRealm(bob))
137	mustAbort(t, "non-hash commit", revive(func() { CommitPvpMove(cross(cur), lobby, "garbage") }), "64 hex")
138	CommitPvpMove(cross(cur), lobby, h("scissors", "b-salt"))
139	testing.SetRealm(testing.NewUserRealm(alice))
140	RevealPvpMove(cross(cur), lobby, "rock", "a-salt")
141	testing.SetRealm(testing.NewUserRealm(bob))
142	mustAbort(t, "reveal different move", revive(func() { RevealPvpMove(cross(cur), lobby, "paper", "b-salt") }), "does not match")
143	if strings.Contains(PvpLobby(lobby), `"status":"finished"`) {
144		t.Errorf("a cheat reveal settled the lobby")
145	}
146}
147
148// H2b: commits overwritable by anyone, any time, on any lobby id.
149func TestH2b_CommitOverwrite(cur realm, t *testing.T) {
150	setup()
151	eve := ta("h2b-eve")
152	testing.SetRealm(testing.NewUserRealm(eve))
153	mustAbort(t, "commit on missing lobby", revive(func() { CommitPvpMove(cross(cur), "pvp-nonexistent-1", h("rock", "x")) }), "not in progress")
154	a, b := ta("h2b-a"), ta("h2b-b")
155	testing.SetRealm(testing.NewUserRealm(a))
156	lobby := CreatePvpLobby(cross(cur), "rps", "grin", "0")
157	testing.SetRealm(testing.NewUserRealm(b))
158	JoinPvpLobby(cross(cur), lobby)
159	testing.SetRealm(testing.NewUserRealm(eve))
160	mustAbort(t, "outsider commit", revive(func() { CommitPvpMove(cross(cur), lobby, h("rock", "x")) }), "not a lobby participant")
161	testing.SetRealm(testing.NewUserRealm(a))
162	CommitPvpMove(cross(cur), lobby, h("rock", "1"))
163	mustAbort(t, "overwrite", revive(func() { CommitPvpMove(cross(cur), lobby, h("paper", "2")) }), "already committed")
164	if pvpLobbies.Get(lobby+":commit:"+a.String()).(string) != h("rock", "1") {
165		t.Errorf("commit overwritten")
166	}
167}
168
169
170
171// H2e: two lobbies in one block got the same id (escrow wiped).
172func TestH2e_LobbyIdCollision(cur realm, t *testing.T) {
173	setup()
174	a, c := ta("coll-alice"), ta("coll-carol")
175	give(a, "grin", 100)
176	give(c, "grin", 100)
177	testing.SetRealm(testing.NewUserRealm(a))
178	id1 := CreatePvpLobby(cross(cur), "rps", "grin", "0")
179	testing.SetRealm(testing.NewUserRealm(c))
180	id2 := CreatePvpLobby(cross(cur), "rps", "grin", "0")
181	if id1 == id2 {
182		t.Errorf("lobby id collision: %s", id1)
183	}
184	if !strings.Contains(PvpLobby(id1), a.String()) {
185		t.Errorf("alice's lobby record lost")
186	}
187}
188
189// NEW (game28): any medal id claimable, tasks completable by anyone.
190func TestNew_AnyMedalClaimable(cur realm, t *testing.T) {
191	setup()
192	p := ta("medal-farmer")
193	payload := `["mythic-legend","made-up-medal"]`
194	testing.SetRealm(testing.NewUserRealm(p))
195	mustAbort(t, "unvouchered medals", revive(func() { BatchClaimAchievements(cross(cur), payload, 1, 1234567890+600, forgedSig) }), "invalid voucher")
196	n, e, s := voucher(t, "ach", p, payload)
197	testing.SetRealm(testing.NewUserRealm(p))
198	got := BatchClaimAchievements(cross(cur), payload, n, e, s) // even with a voucher
199	mustAbort(t, "CompleteTask", revive(func() { CompleteTask(cross(cur), "any-task") }), "operator-only")
200	mustAbort(t, "CreateTask", revive(func() { CreateTask(cross(cur), "any-task", "x") }), "operator only")
201	m := GetMedals(p.String())
202	if got != 0 || strings.Contains(m, "made-up-medal") || strings.Contains(m, "mythic-legend") {
203		t.Errorf("non-allowlisted medal claimed: %s", m)
204	}
205}
206
207var _ = chain.NewCoin
208
209// H2c: no casino refund once the server seed is revealed (two-sided commit/reveal).
210func TestH2c_CasinoRefundAfterReveal(cur realm, t *testing.T) {
211	adm := setup()
212	seedCasino("joy", 1000000)
213	p := ta("h2c-gambler")
214	give(p, "joy", 100)
215	testing.SetRealm(testing.NewUserRealm(adm))
216	CommitCasinoSeed(cross(cur), "round-1", p.String(), h0("serverseed"))
217	testing.SetRealm(testing.NewUserRealm(p))
218	PlaceCasinoBet(cross(cur), "round-1", "coin_flip", "joy", "100", "heads", h0("cs"))
219	testing.SetRealm(testing.NewUserRealm(adm))
220	RevealCasinoSeed(cross(cur), "round-1", "serverseed")
221	after := bal(p, "joy")
222	testing.SkipHeights(2000)
223	testing.SetRealm(testing.NewUserRealm(p))
224	mustAbort(t, "refund after reveal", revive(func() { RefundCasinoBet(cross(cur), "round-1") }), "no refunds")
225	if bal(p, "joy") != after {
226		t.Errorf("refund after reveal paid out")
227	}
228}
229
230// H2d: one bet per round, bound to its player; no bet after the reveal.
231func TestH2d_BetAfterRevealAndOverwrite(cur realm, t *testing.T) {
232	adm := setup()
233	seedCasino("joy", 1000000)
234	p, q := ta("h2d-gambler"), ta("h2d-other")
235	give(p, "joy", 100)
236	give(q, "joy", 100)
237	testing.SetRealm(testing.NewUserRealm(adm))
238	CommitCasinoSeed(cross(cur), "round-2", p.String(), h0("s2"))
239	testing.SetRealm(testing.NewUserRealm(q))
240	mustAbort(t, "other player's round", revive(func() { PlaceCasinoBet(cross(cur), "round-2", "coin_flip", "joy", "10", "heads", h0("x")) }), "another player")
241	testing.SetRealm(testing.NewUserRealm(p))
242	PlaceCasinoBet(cross(cur), "round-2", "coin_flip", "joy", "40", "heads", h0("cs"))
243	mustAbort(t, "second bet", revive(func() { PlaceCasinoBet(cross(cur), "round-2", "coin_flip", "joy", "60", "tails", h0("cs")) }), "")
244	testing.SetRealm(testing.NewUserRealm(adm))
245	RevealCasinoSeed(cross(cur), "round-2", "s2")
246	testing.SetRealm(testing.NewUserRealm(p))
247	mustAbort(t, "bet after reveal", revive(func() { PlaceCasinoBet(cross(cur), "round-2", "coin_flip", "joy", "10", "heads", h0("cs")) }), "")
248	b := casinoBets.Get("round-2:" + p.String()).(*casinoBet)
249	if b.amount != 40 {
250		t.Errorf("first bet overwritten: %d", b.amount)
251	}
252}