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.

upkeep_test.gno

14.30 Kb · 452 lines
  1package upkeep
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"strings"
  7	"testing"
  8
  9	"gno.land/p/nt/testutils/v0"
 10	"gno.land/p/nt/uassert/v0"
 11
 12	subs "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/subscriptions"
 13	guardian "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/timelock_guardian"
 14)
 15
 16var (
 17	testAdmin = testutils.TestAddress("ukadmin")
 18	funder    = testutils.TestAddress("ukfunder")
 19	poker     = testutils.TestAddress("ukpoker")
 20	mallory   = testutils.TestAddress("ukmallory")
 21	provider  = testutils.TestAddress("ukprov")
 22	subber    = testutils.TestAddress("uksubber")
 23	towner    = testutils.TestAddress("uktowner")
 24)
 25
 26// Harness facts, carried from the sibling suites: init runs with an
 27// empty origin (real deploys capture the signer); realm globals persist
 28// across tests while the banker resets; SetRealm binds to the CALLING
 29// frame only, so arming is inlined at every site; SkipHeights advances
 30// the clock 5s per height AND wipes the origin context.
 31func init() {
 32	admin = testAdmin
 33	sweeper = testAdmin
 34	self = chain.PackageAddress("gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/upkeep")
 35}
 36
 37func ugnot(n int64) chain.Coins { return chain.NewCoins(chain.NewCoin(Denom, n)) }
 38
 39func coinsOf(a address) int64 { return banker.NewReadonlyBanker().GetCoin(a, Denom) }
 40
 41// conserved asserts the realm's monetary invariant: everything the realm
 42// holds is either unspent pot or earned-but-unclaimed balance.
 43func conserved(t *testing.T) {
 44	t.Helper()
 45	if Held() != Pot()+UsersTotal() {
 46		t.Fatalf("conservation violated: Held=%d pot=%d users=%d",
 47			Held(), Pot(), UsersTotal())
 48	}
 49}
 50
 51// syncHeld re-issues the realm's expected holdings. The harness banker
 52// resets between tests while realm globals (pot, ledger) persist, so
 53// each money-touching test starts by making the banker match the books.
 54func syncHeld() {
 55	want := Pot() + UsersTotal()
 56	have := coinsOf(Address())
 57	if want > have {
 58		testing.IssueCoins(Address(), ugnot(want-have))
 59	}
 60}
 61
 62func fundAs(cur realm, from address, amount int64) {
 63	testing.SetRealm(testing.NewUserRealm(from))
 64	testing.SetOriginCaller(from)
 65	testing.SetOriginSend(ugnot(amount))
 66	testing.IssueCoins(Address(), ugnot(amount))
 67	Fund(cross(cur))
 68}
 69
 70func adminSet(cur realm, task string, amount int64) {
 71	testing.SetRealm(testing.NewUserRealm(testAdmin))
 72	testing.SetOriginCaller(testAdmin)
 73	testing.SetOriginSend(nil)
 74	SetReward(cross(cur), task, amount)
 75}
 76
 77// expirableSub drives the REAL subscriptions realm to a state where one
 78// subscription is past its grace window, and returns its id. This is
 79// the downstream fixture for the realm-to-realm tests: nothing is
 80// mocked.
 81func expirableSub(cur realm, t *testing.T) int64 {
 82	t.Helper()
 83	testing.SetRealm(testing.NewUserRealm(provider))
 84	testing.SetOriginCaller(provider)
 85	testing.SetOriginSend(nil)
 86	planID := subs.CreatePlan(cross(cur), "upkeep-fixture", "d", 1, 10, 1000)
 87
 88	testing.SetRealm(testing.NewUserRealm(subber))
 89	testing.SetOriginCaller(subber)
 90	testing.SetOriginSend(ugnot(1))
 91	testing.IssueCoins(subs.Address(), ugnot(1))
 92	subID := subs.Subscribe(cross(cur), planID)
 93
 94	// paidThrough = h+10; expirable at paidThrough+10. Skip well past.
 95	testing.SkipHeights(25)
 96	return subID
 97}
 98
 99// maturedAction drives the REAL guardian realm to a matured timelocked
100// action and returns its id.
101var targetSeq int
102
103func maturedAction(cur realm, t *testing.T) string {
104	t.Helper()
105	targetSeq++
106	name := "uktarget" + string(rune('a'+targetSeq%26))
107	testing.SetRealm(testing.NewUserRealm(towner))
108	testing.SetOriginCaller(towner)
109	testing.SetOriginSend(nil)
110	guardian.RegisterTarget(cross(cur), name, guardian.MinDelayFloor, "")
111
112	testing.SetRealm(testing.NewUserRealm(towner))
113	testing.SetOriginCaller(towner)
114	testing.SetOriginSend(nil)
115	id := guardian.Schedule(cross(cur), name, "poke-fixture", guardian.MinDelayFloor)
116
117	testing.SkipHeights(13) // 65s at 5s/height >= the 60s floor
118	return id
119}
120
121func TestInitialState(t *testing.T) {
122	if Admin() != testAdmin || Pot() != 0 || Pokes() != 0 || Funded() != 0 {
123		t.Fatal("unexpected initial state")
124	}
125	if RewardFor(TaskSubExpire) != 0 || RewardFor(TaskTimelockExecute) != 0 {
126		t.Fatal("rewards must default to 0")
127	}
128	conserved(t)
129}
130
131func TestFundAndRewardConfig(cur realm, t *testing.T) {
132	fundAs(cur, funder, 100000)
133	if Pot() != 100000 || Funded() != 100000 {
134		t.Fatalf("pot=%d funded=%d", Pot(), Funded())
135	}
136	conserved(t)
137
138	testing.SetRealm(testing.NewUserRealm(mallory))
139	testing.SetOriginCaller(mallory)
140	testing.SetOriginSend(nil)
141	uassert.AbortsWithMessage(t, cur, "admin only", func() {
142		SetReward(cross(cur), TaskSubExpire, 100)
143	})
144
145	testing.SetRealm(testing.NewUserRealm(testAdmin))
146	testing.SetOriginCaller(testAdmin)
147	testing.SetOriginSend(nil)
148	uassert.AbortsContains(t, cur, "reward must be in", func() {
149		SetReward(cross(cur), TaskSubExpire, MaxReward+1)
150	})
151	uassert.AbortsContains(t, cur, "unknown task", func() {
152		SetReward(cross(cur), "nosuch", 1)
153	})
154
155	adminSet(cur, TaskSubExpire, 5000)
156	adminSet(cur, TaskTimelockExecute, 7000)
157	if RewardFor(TaskSubExpire) != 5000 || RewardFor(TaskTimelockExecute) != 7000 {
158		t.Fatal("rewards not set")
159	}
160}
161
162// The realm-to-realm happy path: poke the REAL subscriptions realm.
163func TestPokeExpire(cur realm, t *testing.T) {
164	syncHeld()
165	subID := expirableSub(cur, t)
166
167	// Downstream state before: still Active.
168	_, _, _, _, _, _, status := subs.SubInfo(subID)
169	if status != "active" {
170		t.Fatalf("fixture status %q", status)
171	}
172
173	potBefore, balBefore := Pot(), BalanceOf(poker)
174	testing.SetRealm(testing.NewUserRealm(poker))
175	testing.SetOriginCaller(poker)
176	testing.SetOriginSend(nil)
177	PokeExpire(cross(cur), subID)
178
179	// Downstream state changed by OUR realm's crossing call...
180	_, _, _, _, _, _, status = subs.SubInfo(subID)
181	if status != "expired" {
182		t.Fatalf("downstream not expired: %q", status)
183	}
184	// ...and the reward moved pot -> caller balance atomically.
185	if Pot() != potBefore-5000 || BalanceOf(poker) != balBefore+5000 {
186		t.Fatalf("reward accounting wrong: pot %d->%d bal %d->%d",
187			potBefore, Pot(), balBefore, BalanceOf(poker))
188	}
189	if Pokes() != 1 {
190		t.Fatalf("pokes=%d", Pokes())
191	}
192	conserved(t)
193}
194
195// Replay: the downstream realm is the replay guard; a second poke on the
196// same subscription aborts THERE and no reward is paid HERE.
197func TestPokeExpireReplay(cur realm, t *testing.T) {
198	syncHeld()
199	potBefore, balBefore := Pot(), BalanceOf(mallory)
200	testing.SetRealm(testing.NewUserRealm(mallory))
201	testing.SetOriginCaller(mallory)
202	testing.SetOriginSend(nil)
203	uassert.AbortsContains(t, cur, "subscription is expired", func() {
204		PokeExpire(cross(cur), 0) // the sub expired in TestPokeExpire
205	})
206	if Pot() != potBefore || BalanceOf(mallory) != balBefore || Pokes() != 1 {
207		t.Fatal("aborted poke mutated reward state")
208	}
209	conserved(t)
210}
211
212// A premature poke aborts downstream ("not expirable") and pays nothing.
213func TestPokeExpirePremature(cur realm, t *testing.T) {
214	syncHeld()
215	testing.SetRealm(testing.NewUserRealm(provider))
216	testing.SetOriginCaller(provider)
217	testing.SetOriginSend(nil)
218	planID := subs.CreatePlan(cross(cur), "fresh", "d", 1, 5000, 1000)
219	testing.SetRealm(testing.NewUserRealm(subber))
220	testing.SetOriginCaller(subber)
221	testing.SetOriginSend(ugnot(1))
222	testing.IssueCoins(subs.Address(), ugnot(1))
223	freshID := subs.Subscribe(cross(cur), planID)
224
225	potBefore := Pot()
226	testing.SetRealm(testing.NewUserRealm(poker))
227	testing.SetOriginCaller(poker)
228	testing.SetOriginSend(nil)
229	uassert.AbortsContains(t, cur, "not expirable", func() {
230		PokeExpire(cross(cur), freshID)
231	})
232	uassert.AbortsContains(t, cur, "unknown subscription id", func() {
233		PokeExpire(cross(cur), 99999)
234	})
235	if Pot() != potBefore {
236		t.Fatal("failed pokes touched the pot")
237	}
238	conserved(t)
239}
240
241// The second realm-to-realm integration: the REAL guardian realm.
242func TestPokeExecute(cur realm, t *testing.T) {
243	syncHeld()
244	id := maturedAction(cur, t)
245	potBefore, balBefore := Pot(), BalanceOf(poker)
246
247	testing.SetRealm(testing.NewUserRealm(poker))
248	testing.SetOriginCaller(poker)
249	testing.SetOriginSend(nil)
250	PokeExecute(cross(cur), id)
251
252	if Pot() != potBefore-7000 || BalanceOf(poker) != balBefore+7000 {
253		t.Fatal("timelock reward accounting wrong")
254	}
255	// Replay refused downstream.
256	testing.SetRealm(testing.NewUserRealm(mallory))
257	testing.SetOriginCaller(mallory)
258	testing.SetOriginSend(nil)
259	uassert.AbortsContains(t, cur, "already executed", func() {
260		PokeExecute(cross(cur), id)
261	})
262	uassert.AbortsContains(t, cur, "action not found", func() {
263		PokeExecute(cross(cur), "tl-nosuch")
264	})
265	// Premature: schedule a new one, poke before maturity.
266	testing.SetRealm(testing.NewUserRealm(towner))
267	testing.SetOriginCaller(towner)
268	testing.SetOriginSend(nil)
269	guardian.RegisterTarget(cross(cur), "ukearly", guardian.MinDelayFloor, "")
270	testing.SetRealm(testing.NewUserRealm(towner))
271	testing.SetOriginCaller(towner)
272	testing.SetOriginSend(nil)
273	early := guardian.Schedule(cross(cur), "ukearly", "x", guardian.MinDelayFloor)
274	testing.SetRealm(testing.NewUserRealm(poker))
275	testing.SetOriginCaller(poker)
276	testing.SetOriginSend(nil)
277	uassert.AbortsContains(t, cur, "too early", func() {
278		PokeExecute(cross(cur), early)
279	})
280	conserved(t)
281}
282
283// Pot exhaustion refuses BEFORE the downstream call: no valve fires
284// unrewarded through this realm. Entering here the pot is deterministic:
285// 100000 funded minus the 5000 and 7000 awarded above = 88000, so four
286// MaxReward pokes leave 8000 < MaxReward and the next must refuse.
287func TestPotExhaustion(cur realm, t *testing.T) {
288	syncHeld()
289	adminSet(cur, TaskSubExpire, MaxReward)
290
291	// Drain by REAL pokes until the pot no longer covers the reward.
292	for i := 0; Pot() >= MaxReward; i++ {
293		if i >= 10 {
294			t.Fatalf("fixture arithmetic broken: pot=%d after %d pokes", Pot(), i)
295		}
296		id := expirableSub(cur, t)
297		testing.SetRealm(testing.NewUserRealm(poker))
298		testing.SetOriginCaller(poker)
299		testing.SetOriginSend(nil)
300		PokeExpire(cross(cur), id)
301	}
302
303	short := expirableSub(cur, t)
304	potBefore := Pot()
305	testing.SetRealm(testing.NewUserRealm(poker))
306	testing.SetOriginCaller(poker)
307	testing.SetOriginSend(nil)
308	uassert.AbortsContains(t, cur, "reward pot cannot cover", func() {
309		PokeExpire(cross(cur), short)
310	})
311	// Downstream MUST be untouched by the refused poke.
312	_, _, _, _, _, _, status := subs.SubInfo(short)
313	if status != "active" {
314		t.Fatal("refused poke reached the downstream realm")
315	}
316	if Pot() != potBefore {
317		t.Fatal("refused poke touched the pot")
318	}
319	conserved(t)
320	adminSet(cur, TaskSubExpire, 5000) // restore for the later tests
321}
322
323// The refusal boundary, deterministic: unknown task, zero reward, and
324// pot-short all refuse before any downstream effect.
325func TestPokeRefusals(cur realm, t *testing.T) {
326	adminSet(cur, TaskSubExpire, 0)
327	testing.SetRealm(testing.NewUserRealm(poker))
328	testing.SetOriginCaller(poker)
329	testing.SetOriginSend(nil)
330	uassert.AbortsContains(t, cur, "no reward configured", func() {
331		PokeExpire(cross(cur), 0)
332	})
333	adminSet(cur, TaskSubExpire, 5000)
334
335	testing.SetRealm(testing.NewUserRealm(poker))
336	testing.SetOriginCaller(poker)
337	testing.SetOriginSend(ugnot(5))
338	uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {
339		PokeExpire(cross(cur), 0)
340	})
341	testing.SetRealm(testing.NewUserRealm(poker))
342	testing.SetOriginCaller(poker)
343	testing.SetOriginSend(ugnot(5))
344	uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {
345		PokeExecute(cross(cur), "x")
346	})
347}
348
349func TestClaims(cur realm, t *testing.T) {
350	syncHeld()
351	earned := BalanceOf(poker)
352	if earned == 0 {
353		t.Fatal("fixture: poker should have earnings")
354	}
355	before := coinsOf(poker)
356	testing.SetRealm(testing.NewUserRealm(poker))
357	testing.SetOriginCaller(poker)
358	testing.SetOriginSend(nil)
359	ClaimAll(cross(cur))
360	if coinsOf(poker)-before != earned || BalanceOf(poker) != 0 {
361		t.Fatal("claim did not move the earned balance")
362	}
363	testing.SetRealm(testing.NewUserRealm(mallory))
364	testing.SetOriginCaller(mallory)
365	testing.SetOriginSend(nil)
366	uassert.AbortsWithMessage(t, cur, "nothing to claim", func() {
367		ClaimAll(cross(cur))
368	})
369	uassert.AbortsContains(t, cur, "insufficient balance", func() {
370		Claim(cross(cur), 1)
371	})
372	conserved(t)
373}
374
375func TestSweepReserve(cur realm, t *testing.T) {
376	syncHeld()
377	testing.SetRealm(testing.NewUserRealm(testAdmin))
378	testing.SetOriginCaller(testAdmin)
379	testing.SetOriginSend(nil)
380	uassert.AbortsContains(t, cur, "no surplus to sweep", func() {
381		SweepDenom(cross(cur), Denom)
382	})
383	testing.IssueCoins(Address(), ugnot(77))
384	reserveBefore := Pot() + UsersTotal()
385	got := coinsOf(testAdmin)
386	testing.SetRealm(testing.NewUserRealm(testAdmin))
387	testing.SetOriginCaller(testAdmin)
388	testing.SetOriginSend(nil)
389	SweepDenom(cross(cur), Denom)
390	if coinsOf(testAdmin)-got != 77 {
391		t.Fatal("sweep amount wrong")
392	}
393	if Pot()+UsersTotal() != reserveBefore {
394		t.Fatal("sweep touched the reserve")
395	}
396	testing.SetRealm(testing.NewUserRealm(mallory))
397	testing.SetOriginCaller(mallory)
398	testing.SetOriginSend(nil)
399	uassert.AbortsWithMessage(t, cur, "sweeper only", func() {
400		SweepDenom(cross(cur), Denom)
401	})
402	conserved(t)
403}
404
405func TestAdminHandover(cur realm, t *testing.T) {
406	testing.SetRealm(testing.NewUserRealm(testAdmin))
407	testing.SetOriginCaller(testAdmin)
408	testing.SetOriginSend(nil)
409	TransferAdmin(cross(cur), funder)
410	testing.SetRealm(testing.NewUserRealm(mallory))
411	testing.SetOriginCaller(mallory)
412	testing.SetOriginSend(nil)
413	uassert.AbortsWithMessage(t, cur, "only the staged successor may accept", func() {
414		AcceptAdmin(cross(cur))
415	})
416	testing.SetRealm(testing.NewUserRealm(funder))
417	testing.SetOriginCaller(funder)
418	testing.SetOriginSend(nil)
419	AcceptAdmin(cross(cur))
420	if Admin() != funder {
421		t.Fatal("authority did not move")
422	}
423	// Former admin locked out; sweeper moved with admin.
424	testing.SetRealm(testing.NewUserRealm(testAdmin))
425	testing.SetOriginCaller(testAdmin)
426	testing.SetOriginSend(nil)
427	uassert.AbortsWithMessage(t, cur, "admin only", func() {
428		SetReward(cross(cur), TaskSubExpire, 1)
429	})
430	uassert.AbortsWithMessage(t, cur, "sweeper only", func() {
431		SweepDenom(cross(cur), Denom)
432	})
433	// Hand back.
434	testing.SetRealm(testing.NewUserRealm(funder))
435	testing.SetOriginCaller(funder)
436	testing.SetOriginSend(nil)
437	TransferAdmin(cross(cur), testAdmin)
438	testing.SetRealm(testing.NewUserRealm(testAdmin))
439	testing.SetOriginCaller(testAdmin)
440	testing.SetOriginSend(nil)
441	AcceptAdmin(cross(cur))
442}
443
444func TestRender(t *testing.T) {
445	out := Render("")
446	if !strings.Contains(out, "sub_expire") || !strings.Contains(out, "timelock_execute") {
447		t.Fatal("render missing task lines")
448	}
449	if Render("x") == Render("") {
450		t.Fatal("unknown path fell through")
451	}
452}