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

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}