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}