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.

timelock_guardian_test.gno

31.67 Kb · 908 lines
  1package timelock_guardian
  2
  3import (
  4	"chain"
  5	"strings"
  6	"testing"
  7
  8	"gno.land/p/nt/testutils/v0"
  9	"gno.land/p/nt/uassert/v0"
 10)
 11
 12var (
 13	owner    = testutils.TestAddress("owner")
 14	guardian = testutils.TestAddress("guardian")
 15	attacker = testutils.TestAddress("attacker")
 16	newOwner = testutils.TestAddress("newowner")
 17)
 18
 19// resetState resets all package-level state between tests.
 20func resetState() {
 21	targets = make(map[string]*TargetConfig)
 22	targetNames = []string{}
 23	ownerTargets = make(map[address]int)
 24	actions = make(map[string]*Action)
 25	actionIDs = []string{}
 26	pending = make(map[string][]string)
 27	pendingTargets = []string{}
 28	ptIndex = make(map[string]int)
 29	nextID = 1
 30}
 31
 32// heights advancing time: SkipHeights moves the clock 5s per height.
 33func skipSeconds(sec int64) {
 34	testing.SkipHeights((sec + 4) / 5)
 35}
 36
 37func registerUpgrades(cur realm) string {
 38	testing.SetRealm(testing.NewUserRealm(owner))
 39	RegisterTarget(cross(cur), "upgrades", MinDelayFloor, guardian)
 40	return "upgrades"
 41}
 42
 43// ---------- RegisterTarget ----------
 44
 45func TestRegisterTarget_Basic(cur realm, t *testing.T) {
 46	resetState()
 47	testing.SetRealm(testing.NewUserRealm(owner))
 48
 49	RegisterTarget(cross(cur), "upgrades", 120, guardian)
 50
 51	tc := targets["upgrades"]
 52	uassert.Equal(t, string(owner), string(tc.Owner))
 53	uassert.Equal(t, string(guardian), string(tc.Guardian))
 54	uassert.Equal(t, int64(120), tc.MinDelay)
 55}
 56
 57func TestRegisterTarget_NoGuardianAllowed(cur realm, t *testing.T) {
 58	resetState()
 59	testing.SetRealm(testing.NewUserRealm(owner))
 60
 61	RegisterTarget(cross(cur), "plain", MinDelayFloor, "")
 62	uassert.Equal(t, "", string(targets["plain"].Guardian))
 63}
 64
 65func TestRegisterTarget_DuplicateRejected(cur realm, t *testing.T) {
 66	resetState()
 67	testing.SetRealm(testing.NewUserRealm(owner))
 68	registerUpgrades(cur)
 69
 70	uassert.AbortsWithMessage(t, cur,
 71		"target already registered: upgrades",
 72		func() { RegisterTarget(cross(cur), "upgrades", MinDelayFloor, "") },
 73	)
 74}
 75
 76func TestRegisterTarget_InvalidNameRejected(cur realm, t *testing.T) {
 77	resetState()
 78	testing.SetRealm(testing.NewUserRealm(owner))
 79
 80	for _, bad := range []string{"", "Upgrades", "has space", strings.Repeat("a", MaxTargetNameLen+1)} {
 81		uassert.AbortsContains(t, cur,
 82			"target name must be",
 83			func() { RegisterTarget(cross(cur), bad, MinDelayFloor, "") },
 84		)
 85	}
 86}
 87
 88func TestRegisterTarget_DelayBoundsEnforced(cur realm, t *testing.T) {
 89	resetState()
 90	testing.SetRealm(testing.NewUserRealm(owner))
 91
 92	uassert.AbortsContains(t, cur,
 93		"minimum delay must be at least",
 94		func() { RegisterTarget(cross(cur), "too_fast", MinDelayFloor-1, "") },
 95	)
 96	uassert.AbortsContains(t, cur,
 97		"minimum delay exceeds the maximum",
 98		func() { RegisterTarget(cross(cur), "too_slow", MaxDelay+1, "") },
 99	)
100}
101
102func TestRegisterTarget_InvalidGuardianRejected(cur realm, t *testing.T) {
103	resetState()
104	testing.SetRealm(testing.NewUserRealm(owner))
105
106	uassert.AbortsWithMessage(t, cur,
107		"invalid guardian address: junk",
108		func() { RegisterTarget(cross(cur), "upgrades", MinDelayFloor, address("junk")) },
109	)
110}
111
112// ---------- SetGuardian / two-step TransferTargetOwnership ----------
113
114func TestSetGuardian_OwnerOnly(cur realm, t *testing.T) {
115	resetState()
116	testing.SetRealm(testing.NewUserRealm(owner))
117	registerUpgrades(cur)
118
119	testing.SetRealm(testing.NewUserRealm(attacker))
120	uassert.AbortsWithMessage(t, cur,
121		"only the target owner can set the guardian",
122		func() { SetGuardian(cross(cur), "upgrades", attacker) },
123	)
124
125	testing.SetRealm(testing.NewUserRealm(owner))
126	SetGuardian(cross(cur), "upgrades", "")
127	uassert.Equal(t, "", string(targets["upgrades"].Guardian))
128}
129
130func TestTransferTargetOwnership_TwoStep(cur realm, t *testing.T) {
131	resetState()
132	testing.SetRealm(testing.NewUserRealm(owner))
133	registerUpgrades(cur)
134
135	testing.SetRealm(testing.NewUserRealm(attacker))
136	uassert.AbortsWithMessage(t, cur,
137		"only the target owner can transfer ownership",
138		func() { TransferTargetOwnership(cross(cur), "upgrades", attacker) },
139	)
140
141	testing.SetRealm(testing.NewUserRealm(owner))
142	uassert.AbortsWithMessage(t, cur,
143		"invalid new owner address: junk",
144		func() { TransferTargetOwnership(cross(cur), "upgrades", address("junk")) },
145	)
146
147	uassert.AbortsWithMessage(t, cur,
148		"new owner is already the owner",
149		func() { TransferTargetOwnership(cross(cur), "upgrades", owner) },
150	)
151
152	// the offer alone changes nothing
153	TransferTargetOwnership(cross(cur), "upgrades", newOwner)
154	uassert.Equal(t, string(owner), string(targets["upgrades"].Owner))
155	uassert.Equal(t, string(newOwner), string(targets["upgrades"].PendingOwner))
156	uassert.Equal(t, 1, ownerTargets[owner])
157	uassert.Equal(t, 0, ownerTargets[newOwner])
158
159	// only the nominee can accept
160	testing.SetRealm(testing.NewUserRealm(attacker))
161	uassert.AbortsWithMessage(t, cur,
162		"caller is not the pending owner",
163		func() { AcceptTargetOwnership(cross(cur), "upgrades") },
164	)
165
166	testing.SetRealm(testing.NewUserRealm(newOwner))
167	AcceptTargetOwnership(cross(cur), "upgrades")
168	uassert.Equal(t, string(newOwner), string(targets["upgrades"].Owner))
169	uassert.Equal(t, "", string(targets["upgrades"].PendingOwner))
170	uassert.Equal(t, 0, ownerTargets[owner])
171	uassert.Equal(t, 1, ownerTargets[newOwner])
172}
173
174func TestTransferTargetOwnership_ClearOffer(cur realm, t *testing.T) {
175	resetState()
176	testing.SetRealm(testing.NewUserRealm(owner))
177	registerUpgrades(cur)
178
179	TransferTargetOwnership(cross(cur), "upgrades", newOwner)
180	TransferTargetOwnership(cross(cur), "upgrades", "") // clear
181
182	testing.SetRealm(testing.NewUserRealm(newOwner))
183	uassert.AbortsWithMessage(t, cur,
184		"caller is not the pending owner",
185		func() { AcceptTargetOwnership(cross(cur), "upgrades") },
186	)
187}
188
189func TestTransferTargetOwnership_QuotaCheckedAtAccept(cur realm, t *testing.T) {
190	resetState()
191	testing.SetRealm(testing.NewUserRealm(newOwner))
192	for i := 0; i < MaxTargetsPerOwner; i++ {
193		RegisterTarget(cross(cur), "t_"+strings.Repeat("x", i+1), MinDelayFloor, "")
194	}
195
196	testing.SetRealm(testing.NewUserRealm(owner))
197	registerUpgrades(cur)
198	TransferTargetOwnership(cross(cur), "upgrades", newOwner) // offer succeeds
199
200	// ...but accepting past the quota is refused, and ownership is unchanged
201	testing.SetRealm(testing.NewUserRealm(newOwner))
202	uassert.AbortsWithMessage(t, cur,
203		"accepting would exceed the per-owner target limit",
204		func() { AcceptTargetOwnership(cross(cur), "upgrades") },
205	)
206	uassert.Equal(t, string(owner), string(targets["upgrades"].Owner))
207}
208
209func TestUnsolicitedOfferConsumesNothing(cur realm, t *testing.T) {
210	resetState()
211	victim := testutils.TestAddress("victim")
212
213	// fix Y5 regression: an attacker offering targets to a victim must
214	// not consume the victim's quota or change any of their state
215	testing.SetRealm(testing.NewUserRealm(attacker))
216	for i := 0; i < MaxTargetsPerOwner; i++ {
217		name := "junk_" + strings.Repeat("x", i+1)
218		RegisterTarget(cross(cur), name, MinDelayFloor, "")
219		TransferTargetOwnership(cross(cur), name, victim)
220	}
221	uassert.Equal(t, 0, ownerTargets[victim])
222
223	// the victim can still register a full quota of their own targets
224	testing.SetRealm(testing.NewUserRealm(victim))
225	for i := 0; i < MaxTargetsPerOwner; i++ {
226		RegisterTarget(cross(cur), "mine_"+strings.Repeat("y", i+1), MinDelayFloor, "")
227	}
228}
229
230// ---------- Schedule ----------
231
232func TestSchedule_OwnerOnly(cur realm, t *testing.T) {
233	resetState()
234	testing.SetRealm(testing.NewUserRealm(owner))
235	registerUpgrades(cur)
236
237	// an attacker cannot schedule an action against someone else's
238	// target — the core binding the audit found missing
239	testing.SetRealm(testing.NewUserRealm(attacker))
240	uassert.AbortsWithMessage(t, cur,
241		"only the target owner can schedule actions for upgrades",
242		func() { Schedule(cross(cur), "upgrades", "malicious upgrade", MinDelayFloor) },
243	)
244}
245
246func TestSchedule_UnregisteredTargetRejected(cur realm, t *testing.T) {
247	resetState()
248	testing.SetRealm(testing.NewUserRealm(owner))
249
250	uassert.AbortsWithMessage(t, cur,
251		"target not found: ghost",
252		func() { Schedule(cross(cur), "ghost", "data", MinDelayFloor) },
253	)
254}
255
256func TestSchedule_MinDelayEnforced(cur realm, t *testing.T) {
257	resetState()
258	testing.SetRealm(testing.NewUserRealm(owner))
259	RegisterTarget(cross(cur), "upgrades", 3600, "")
260
261	// a delay below the target's registered minimum is rejected — no
262	// more 1-second "timelocks"
263	uassert.AbortsWithMessage(t, cur,
264		"delay below the target's minimum of 3600 seconds",
265		func() { Schedule(cross(cur), "upgrades", "data", 60) },
266	)
267}
268
269func TestSchedule_MaxDelayEnforced(cur realm, t *testing.T) {
270	resetState()
271	testing.SetRealm(testing.NewUserRealm(owner))
272	registerUpgrades(cur)
273
274	// overflow regression: a delay past MaxDelay must abort, never wrap
275	// ExecuteAfter into the past
276	uassert.AbortsContains(t, cur,
277		"delay exceeds the maximum",
278		func() { Schedule(cross(cur), "upgrades", "data", MaxDelay+1) },
279	)
280}
281
282func TestSchedule_DataValidation(cur realm, t *testing.T) {
283	resetState()
284	testing.SetRealm(testing.NewUserRealm(owner))
285	registerUpgrades(cur)
286
287	uassert.AbortsWithMessage(t, cur,
288		"data must not be empty",
289		func() { Schedule(cross(cur), "upgrades", "", MinDelayFloor) },
290	)
291	uassert.AbortsContains(t, cur,
292		"data exceeds",
293		func() { Schedule(cross(cur), "upgrades", strings.Repeat("x", MaxDataLen+1), MinDelayFloor) },
294	)
295}
296
297func TestSchedule_PendingCap(cur realm, t *testing.T) {
298	resetState()
299	testing.SetRealm(testing.NewUserRealm(owner))
300	registerUpgrades(cur)
301
302	for i := 0; i < MaxPendingPerTarget; i++ {
303		Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
304	}
305	uassert.AbortsWithMessage(t, cur,
306		"too many pending actions for target upgrades",
307		func() { Schedule(cross(cur), "upgrades", "op", MinDelayFloor) },
308	)
309}
310
311func TestSchedule_SetsWindows(cur realm, t *testing.T) {
312	resetState()
313	testing.SetRealm(testing.NewUserRealm(owner))
314	registerUpgrades(cur)
315
316	id := Schedule(cross(cur), "upgrades", "rotate keys", 120)
317	a := actions[id]
318	uassert.Equal(t, int64(120), int64(a.ExecuteAfter.Sub(a.ScheduledAt).Seconds()))
319	uassert.Equal(t, GracePeriod, int64(a.ExpiresAt.Sub(a.ExecuteAfter).Seconds()))
320}
321
322// ---------- Execute ----------
323
324func TestExecute_TooEarlyBlocked(cur realm, t *testing.T) {
325	resetState()
326	testing.SetRealm(testing.NewUserRealm(owner))
327	registerUpgrades(cur)
328	id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
329
330	uassert.AbortsContains(t, cur, "too early", func() { Execute(cross(cur), id) })
331	uassert.False(t, IsReady(id), "must not be ready before the delay")
332}
333
334func TestExecute_ReadyAfterDelay_AnyoneCanExecute(cur realm, t *testing.T) {
335	resetState()
336	testing.SetRealm(testing.NewUserRealm(owner))
337	registerUpgrades(cur)
338	id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
339
340	skipSeconds(MinDelayFloor)
341	uassert.True(t, IsReady(id), "must be ready after the delay")
342
343	// executor identity is deliberately unrestricted
344	testing.SetRealm(testing.NewUserRealm(attacker))
345	uassert.Equal(t, "executed: "+id, Execute(cross(cur), id))
346	uassert.True(t, IsExecuted(id), "must record execution")
347}
348
349func TestExecute_TwiceBlocked(cur realm, t *testing.T) {
350	resetState()
351	testing.SetRealm(testing.NewUserRealm(owner))
352	registerUpgrades(cur)
353	id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
354	skipSeconds(MinDelayFloor)
355	Execute(cross(cur), id)
356
357	uassert.AbortsWithMessage(t, cur, "action already executed", func() { Execute(cross(cur), id) })
358}
359
360func TestExecute_ExpiredBlocked(cur realm, t *testing.T) {
361	resetState()
362	testing.SetRealm(testing.NewUserRealm(owner))
363	registerUpgrades(cur)
364	id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
365
366	// travel past the grace window: stale actions cannot be sprung later
367	skipSeconds(MinDelayFloor + GracePeriod + 10)
368	uassert.False(t, IsReady(id), "expired action must not be ready")
369	uassert.AbortsContains(t, cur, "action expired", func() { Execute(cross(cur), id) })
370	uassert.Equal(t, "none", GetPending())
371}
372
373// ---------- Cancel / Veto ----------
374
375func TestCancel_CurrentOwnerOnly(cur realm, t *testing.T) {
376	resetState()
377	testing.SetRealm(testing.NewUserRealm(owner))
378	registerUpgrades(cur)
379	id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
380
381	testing.SetRealm(testing.NewUserRealm(attacker))
382	uassert.AbortsWithMessage(t, cur,
383		"only the target owner can cancel",
384		func() { Cancel(cross(cur), id) },
385	)
386
387	testing.SetRealm(testing.NewUserRealm(owner))
388	uassert.Equal(t, "cancelled: "+id, Cancel(cross(cur), id))
389	_, present := actions[id]
390	uassert.False(t, present, "cancelled action must be reaped from state")
391
392	// re-audit P3: after an ownership transfer the SCHEDULING creator
393	// loses cancel rights; only the current owner holds them
394	id2 := Schedule(cross(cur), "upgrades", "op2", MinDelayFloor)
395	TransferTargetOwnership(cross(cur), "upgrades", newOwner)
396	testing.SetRealm(testing.NewUserRealm(newOwner))
397	AcceptTargetOwnership(cross(cur), "upgrades")
398	testing.SetRealm(testing.NewUserRealm(owner))
399	uassert.AbortsWithMessage(t, cur,
400		"only the target owner can cancel",
401		func() { Cancel(cross(cur), id2) },
402	)
403	testing.SetRealm(testing.NewUserRealm(newOwner))
404	uassert.Equal(t, "cancelled: "+id2, Cancel(cross(cur), id2))
405}
406
407func TestCancel_ExecutedBlocked(cur realm, t *testing.T) {
408	resetState()
409	testing.SetRealm(testing.NewUserRealm(owner))
410	registerUpgrades(cur)
411	id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
412	skipSeconds(MinDelayFloor)
413	Execute(cross(cur), id)
414
415	uassert.AbortsWithMessage(t, cur,
416		"cannot cancel an already-executed action",
417		func() { Cancel(cross(cur), id) },
418	)
419}
420
421func TestVeto_GuardianOnly(cur realm, t *testing.T) {
422	resetState()
423	testing.SetRealm(testing.NewUserRealm(owner))
424	registerUpgrades(cur)
425	id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
426
427	testing.SetRealm(testing.NewUserRealm(attacker))
428	uassert.AbortsWithMessage(t, cur,
429		"only the target guardian can veto",
430		func() { Veto(cross(cur), id) },
431	)
432
433	testing.SetRealm(testing.NewUserRealm(guardian))
434	uassert.Equal(t, "vetoed: "+id, Veto(cross(cur), id))
435	_, present := actions[id]
436	uassert.False(t, present, "vetoed action must be reaped from state")
437
438	// a vetoed action can never execute
439	skipSeconds(MinDelayFloor)
440	uassert.AbortsWithMessage(t, cur, "action not found: "+id, func() { Execute(cross(cur), id) })
441}
442
443func TestVeto_NoGuardianTargetBlocked(cur realm, t *testing.T) {
444	resetState()
445	testing.SetRealm(testing.NewUserRealm(owner))
446	RegisterTarget(cross(cur), "plain", MinDelayFloor, "")
447	id := Schedule(cross(cur), "plain", "op", MinDelayFloor)
448
449	testing.SetRealm(testing.NewUserRealm(guardian))
450	uassert.AbortsWithMessage(t, cur,
451		"target has no guardian: plain",
452		func() { Veto(cross(cur), id) },
453	)
454}
455
456// ---------- Expire (fix Y2) ----------
457
458func TestExpire_PermissionlessRecovery(cur realm, t *testing.T) {
459	resetState()
460	testing.SetRealm(testing.NewUserRealm(owner))
461	registerUpgrades(cur)
462
463	// fill the target's quota, then let everything expire
464	var ids []string
465	for i := 0; i < MaxPendingPerTarget; i++ {
466		ids = append(ids, Schedule(cross(cur), "upgrades", "op", MinDelayFloor))
467	}
468	skipSeconds(MinDelayFloor + GracePeriod + 10)
469
470	// fix Y2 regression: ANYONE — not just a lucky sweep window — can
471	// reap a provably expired action and free the slot it wedges
472	testing.SetRealm(testing.NewUserRealm(attacker))
473	uassert.Equal(t, "expired: "+ids[0], Expire(cross(cur), ids[0]))
474	_, present := actions[ids[0]]
475	uassert.False(t, present, "expired action must be reaped from state")
476
477	// reap the rest as the owner; quota and SetGuardian both recover
478	testing.SetRealm(testing.NewUserRealm(owner))
479	for _, id := range ids[1:] {
480		Expire(cross(cur), id)
481	}
482	uassert.Equal(t, 0, pendingCount("upgrades"))
483	SetGuardian(cross(cur), "upgrades", "") // no longer blocked
484	Schedule(cross(cur), "upgrades", "fresh", MinDelayFloor)
485}
486
487func TestExpire_RefusesLiveAndExecuted(cur realm, t *testing.T) {
488	resetState()
489	testing.SetRealm(testing.NewUserRealm(owner))
490	registerUpgrades(cur)
491	live := Schedule(cross(cur), "upgrades", "live", MinDelayFloor)
492	done := Schedule(cross(cur), "upgrades", "done", MinDelayFloor)
493	skipSeconds(MinDelayFloor)
494	Execute(cross(cur), done)
495
496	uassert.AbortsWithMessage(t, cur, "action is not expired",
497		func() { Expire(cross(cur), live) })
498	uassert.AbortsWithMessage(t, cur, "action already executed",
499		func() { Expire(cross(cur), done) })
500	uassert.AbortsWithMessage(t, cur, "action not found: ghost",
501		func() { Expire(cross(cur), "ghost") })
502}
503
504// ---------- stray sends (fix Y6) ----------
505
506func TestStraySendRejectedEverywhere(cur realm, t *testing.T) {
507	resetState()
508	testing.SetRealm(testing.NewUserRealm(owner))
509	registerUpgrades(cur)
510	id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
511
512	// fix Y6 regression: this realm holds no banker, so an attached
513	// send would strand on the realm address forever — every crossing
514	// function must abort it (the abort reverts the transfer)
515	msg := "this realm does not accept coins"
516	testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: 5}})
517	uassert.AbortsWithMessage(t, cur, msg, func() { RegisterTarget(cross(cur), "paid", MinDelayFloor, "") })
518	uassert.AbortsWithMessage(t, cur, msg, func() { Schedule(cross(cur), "upgrades", "op2", MinDelayFloor) })
519	uassert.AbortsWithMessage(t, cur, msg, func() { Cancel(cross(cur), id) })
520	uassert.AbortsWithMessage(t, cur, msg, func() { SetGuardian(cross(cur), "upgrades", "") })
521	uassert.AbortsWithMessage(t, cur, msg, func() { TransferTargetOwnership(cross(cur), "upgrades", newOwner) })
522	uassert.AbortsWithMessage(t, cur, msg, func() { AcceptTargetOwnership(cross(cur), "upgrades") })
523	uassert.AbortsWithMessage(t, cur, msg, func() { Veto(cross(cur), id) })
524	uassert.AbortsWithMessage(t, cur, msg, func() { Expire(cross(cur), id) })
525	// SkipHeights resets the test context (wiping the send envelope),
526	// so re-arm it before probing the ready-action Execute path
527	skipSeconds(MinDelayFloor)
528	testing.SetRealm(testing.NewUserRealm(owner))
529	testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: 5}})
530	uassert.AbortsWithMessage(t, cur, msg, func() { Execute(cross(cur), id) })
531	testing.SetOriginSend(nil)
532
533	// without coins the same call succeeds
534	uassert.Equal(t, "executed: "+id, Execute(cross(cur), id))
535}
536
537// ---------- queries + render ----------
538
539func TestGetPending_ExcludesFinished(cur realm, t *testing.T) {
540	resetState()
541	testing.SetRealm(testing.NewUserRealm(owner))
542	registerUpgrades(cur)
543	id1 := Schedule(cross(cur), "upgrades", "op1", MinDelayFloor)
544	id2 := Schedule(cross(cur), "upgrades", "op2", MinDelayFloor)
545	id3 := Schedule(cross(cur), "upgrades", "op3", MinDelayFloor)
546
547	Cancel(cross(cur), id2)
548	skipSeconds(MinDelayFloor)
549	Execute(cross(cur), id1)
550
551	uassert.Equal(t, id3, GetPending())
552}
553
554func TestGetPending_GroupedByTarget(cur realm, t *testing.T) {
555	resetState()
556	testing.SetRealm(testing.NewUserRealm(owner))
557	RegisterTarget(cross(cur), "alpha", MinDelayFloor, "")
558	RegisterTarget(cross(cur), "beta", MinDelayFloor, "")
559
560	// interleave scheduling across targets; output groups by target in
561	// registration order (fix Y4 bookkeeping), insertion-ordered within
562	a1 := Schedule(cross(cur), "alpha", "a1", MinDelayFloor)
563	b1 := Schedule(cross(cur), "beta", "b1", MinDelayFloor)
564	a2 := Schedule(cross(cur), "alpha", "a2", MinDelayFloor)
565
566	uassert.Equal(t, a1+", "+a2+", "+b1, GetPending())
567	uassert.Equal(t, 2, pendingCount("alpha"))
568	uassert.Equal(t, 1, pendingCount("beta"))
569
570	Cancel(cross(cur), a1)
571	uassert.Equal(t, a2+", "+b1, GetPending())
572	uassert.Equal(t, 1, pendingCount("alpha"))
573}
574
575func TestGetTarget_Summary(cur realm, t *testing.T) {
576	resetState()
577	testing.SetRealm(testing.NewUserRealm(owner))
578	registerUpgrades(cur)
579
580	out := GetTarget("upgrades")
581	uassert.True(t, strings.Contains(out, string(owner)), "must show owner")
582	uassert.True(t, strings.Contains(out, string(guardian)), "must show guardian")
583	uassert.True(t, strings.Contains(out, "Min delay: 60s"), "must show min delay")
584
585	TransferTargetOwnership(cross(cur), "upgrades", newOwner)
586	uassert.True(t, strings.Contains(GetTarget("upgrades"), "Pending owner: "+string(newOwner)),
587		"must show a pending ownership offer")
588}
589
590func TestRender_SanitizesInjection(cur realm, t *testing.T) {
591	resetState()
592	testing.SetRealm(testing.NewUserRealm(owner))
593	registerUpgrades(cur)
594	Schedule(cross(cur), "upgrades", "evil` | injected\nrow", MinDelayFloor)
595
596	out := Render("")
597	uassert.False(t, strings.Contains(out, "evil`"), "backticks must be sanitized")
598	uassert.False(t, strings.Contains(out, "injected\nrow"), "newlines must be sanitized")
599}
600
601func TestRender_SanitizesHTMLBrackets(cur realm, t *testing.T) {
602	resetState()
603	testing.SetRealm(testing.NewUserRealm(owner))
604	registerUpgrades(cur)
605	// fix Y3 regression: raw HTML must never reach the page
606	Schedule(cross(cur), "upgrades", "<script>alert(1)</script>", MinDelayFloor)
607
608	out := Render("")
609	uassert.False(t, strings.Contains(out, "<script"), "script tags must be sanitized in Render")
610	uassert.False(t, strings.Contains(out, "</script"), "closing tags must be sanitized in Render")
611	one := GetAction(GetPending())
612	uassert.False(t, strings.Contains(one, "<script"), "script tags must be sanitized in GetAction")
613}
614
615func TestRender_EmptyState(cur realm, t *testing.T) {
616	resetState()
617	uassert.Equal(t, "# Timelock Guardian\n\nNo actions scheduled.\n", Render(""))
618}
619
620func TestRender_Sections(cur realm, t *testing.T) {
621	resetState()
622	testing.SetRealm(testing.NewUserRealm(owner))
623	registerUpgrades(cur)
624	id1 := Schedule(cross(cur), "upgrades", "op1", MinDelayFloor)
625	id2 := Schedule(cross(cur), "upgrades", "op2", MinDelayFloor)
626	Schedule(cross(cur), "upgrades", "op3", MinDelayFloor)
627	skipSeconds(MinDelayFloor)
628	Execute(cross(cur), id1)
629	Cancel(cross(cur), id2)
630
631	out := Render("")
632	uassert.True(t, strings.Contains(out, "## Executed (1)"), "executed section")
633	uassert.True(t, strings.Contains(out, "## Pending (1)"), "pending section")
634	uassert.False(t, strings.Contains(out, "Cancelled"), "cancelled actions are reaped, not rendered")
635}
636
637// ---------- re-audit regressions (2026-09-02) ----------
638
639func TestScheduleCancelCycling_CannotBrickTheRealm(cur realm, t *testing.T) {
640	resetState()
641	testing.SetRealm(testing.NewUserRealm(owner))
642	registerUpgrades(cur)
643
644	// re-audit P1: the old lifetime cap could be consumed forever by
645	// schedule+cancel cycling; the cap now bounds LIVE actions and
646	// cancelled records are reaped, so heavy cycling leaves no residue
647	for i := 0; i < 3*MaxPendingPerTarget; i++ {
648		id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
649		Cancel(cross(cur), id)
650	}
651	uassert.Equal(t, 0, len(actions), "cycling must leave no state residue")
652	uassert.Equal(t, 0, pendingCount("upgrades"), "no live actions after cycling")
653	Schedule(cross(cur), "upgrades", "op", MinDelayFloor) // must not abort
654}
655
656func TestSetGuardian_BlockedWhilePending(cur realm, t *testing.T) {
657	resetState()
658	testing.SetRealm(testing.NewUserRealm(owner))
659	registerUpgrades(cur)
660	id := Schedule(cross(cur), "upgrades", "rug", MinDelayFloor)
661
662	// re-audit P2: the owner must not be able to strip the guardian
663	// during the delay window the guardian exists to police
664	uassert.AbortsWithMessage(t, cur,
665		"cannot change the guardian while actions are pending",
666		func() { SetGuardian(cross(cur), "upgrades", "") },
667	)
668
669	// once nothing is pending, the owner may change the guardian
670	Cancel(cross(cur), id)
671	SetGuardian(cross(cur), "upgrades", "")
672	uassert.Equal(t, "", string(targets["upgrades"].Guardian))
673}
674
675func TestVetoAndCancel_RefuseExpired(cur realm, t *testing.T) {
676	resetState()
677	testing.SetRealm(testing.NewUserRealm(owner))
678	registerUpgrades(cur)
679	id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
680	skipSeconds(MinDelayFloor + GracePeriod + 10)
681
682	// re-audit P3: terminal-state writes on expired actions rewrote
683	// history; both must refuse and point at the Expire valve
684	uassert.AbortsWithMessage(t, cur, "action expired: nothing to cancel (use Expire)",
685		func() { Cancel(cross(cur), id) })
686	testing.SetRealm(testing.NewUserRealm(guardian))
687	uassert.AbortsWithMessage(t, cur, "action expired: nothing to veto (use Expire)",
688		func() { Veto(cross(cur), id) })
689}
690
691func TestExpiredActions_ReapedOnSchedule(cur realm, t *testing.T) {
692	resetState()
693	testing.SetRealm(testing.NewUserRealm(owner))
694	registerUpgrades(cur)
695	Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
696	skipSeconds(MinDelayFloor + GracePeriod + 10)
697
698	// the next Schedule sweeps expired actions out of state
699	Schedule(cross(cur), "upgrades", "op2", MinDelayFloor)
700	uassert.Equal(t, 1, len(actions), "expired action must be reaped by the sweep")
701}
702
703func TestRender_LinkInjectionSanitized(cur realm, t *testing.T) {
704	resetState()
705	testing.SetRealm(testing.NewUserRealm(owner))
706	registerUpgrades(cur)
707	Schedule(cross(cur), "upgrades", "[approve](https://evil.example)", MinDelayFloor)
708
709	out := Render("")
710	uassert.False(t, strings.Contains(out, "[approve]"), "link syntax must be sanitized")
711}
712
713func TestPerOwnerTargetCap(cur realm, t *testing.T) {
714	resetState()
715	testing.SetRealm(testing.NewUserRealm(owner))
716	for i := 0; i < MaxTargetsPerOwner; i++ {
717		RegisterTarget(cross(cur), "t_"+strings.Repeat("x", i+1), MinDelayFloor, "")
718	}
719	uassert.AbortsWithMessage(t, cur,
720		"per-owner target limit reached",
721		func() { RegisterTarget(cross(cur), "one_more", MinDelayFloor, "") },
722	)
723}
724
725// ---------- round-3 audit regressions (2026-09-02) ----------
726
727func TestSybilsCannotExhaustSharedCapacity(cur realm, t *testing.T) {
728	resetState()
729
730	// round-3 P1: two attackers filling their OWN per-owner quotas must
731	// never block a third owner's Schedule — there is no shared cap left
732	// to exhaust
733	sybils := []address{testutils.TestAddress("sybil_a"), testutils.TestAddress("sybil_b")}
734	for _, syb := range sybils {
735		testing.SetRealm(testing.NewUserRealm(syb))
736		for i := 0; i < MaxTargetsPerOwner; i++ {
737			name := "t_" + string(syb[len(syb)-4:]) + "_" + strings.Repeat("x", i+1)
738			RegisterTarget(cross(cur), name, MinDelayFloor, "")
739			for j := 0; j < MaxPendingPerTarget; j++ {
740				Schedule(cross(cur), name, "spam", MaxDelay) // 10y delay: sweep reclaims nothing
741			}
742		}
743	}
744
745	// a legitimate owner is entirely unaffected
746	testing.SetRealm(testing.NewUserRealm(owner))
747	RegisterTarget(cross(cur), "legit", MinDelayFloor, "")
748	Schedule(cross(cur), "legit", "real work", MinDelayFloor) // must not abort
749}
750
751func TestVictimRecoversBehindSybilWall(cur realm, t *testing.T) {
752	resetState()
753
754	// fix Y2/Y4 regression — the exact starvation case the old global
755	// budget-windowed sweep could not recover from: a wall of long-delay
756	// sybil actions in front, a victim whose actions expired behind it.
757	// Per-target sweeps and permissionless Expire make recovery local.
758	syb := testutils.TestAddress("wall")
759	testing.SetRealm(testing.NewUserRealm(syb))
760	for i := 0; i < 5; i++ {
761		name := "wall_" + strings.Repeat("w", i+1)
762		RegisterTarget(cross(cur), name, MinDelayFloor, "")
763		for j := 0; j < MaxPendingPerTarget; j++ {
764			Schedule(cross(cur), name, "wall", MaxDelay) // 100 live 10y entries
765		}
766	}
767
768	victim := testutils.TestAddress("victim2")
769	testing.SetRealm(testing.NewUserRealm(victim))
770	RegisterTarget(cross(cur), "vt", MinDelayFloor, guardian)
771	var ids []string
772	for j := 0; j < MaxPendingPerTarget; j++ {
773		ids = append(ids, Schedule(cross(cur), "vt", "op", MinDelayFloor))
774	}
775	skipSeconds(MinDelayFloor + GracePeriod + 10) // all 20 expire
776
777	// the victim's own next Schedule sweeps THEIR target despite the wall
778	Schedule(cross(cur), "vt", "recovered", MinDelayFloor)
779	uassert.Equal(t, 1, pendingCount("vt"), "expired actions swept despite the sybil wall")
780
781	// and Expire frees a wedged slot directly, from any key
782	id2 := Schedule(cross(cur), "vt", "will expire", MinDelayFloor)
783	testing.SetRealm(testing.NewUserRealm(attacker))
784	skipSeconds(MinDelayFloor + GracePeriod + 10)
785	Expire(cross(cur), id2)
786	_, present := actions[id2]
787	uassert.False(t, present, "Expire must reap regardless of global queue position")
788}
789
790func TestPendingCounterConsistency(cur realm, t *testing.T) {
791	resetState()
792	testing.SetRealm(testing.NewUserRealm(owner))
793	registerUpgrades(cur)
794
795	id1 := Schedule(cross(cur), "upgrades", "a", MinDelayFloor)
796	id2 := Schedule(cross(cur), "upgrades", "b", MinDelayFloor)
797	Schedule(cross(cur), "upgrades", "c", MinDelayFloor)
798	uassert.Equal(t, 3, pendingCount("upgrades"))
799
800	Cancel(cross(cur), id1)
801	uassert.Equal(t, 2, pendingCount("upgrades"))
802	skipSeconds(MinDelayFloor)
803	Execute(cross(cur), id2)
804	uassert.Equal(t, 1, pendingCount("upgrades"))
805
806	// expiry: the count drops when the sweep reaps
807	skipSeconds(GracePeriod + 10)
808	Schedule(cross(cur), "upgrades", "d", MinDelayFloor) // triggers sweep of expired "c"
809	uassert.Equal(t, 1, pendingCount("upgrades"))
810}
811
812func TestSanitize_EmphasisMarkers(cur realm, t *testing.T) {
813	resetState()
814	testing.SetRealm(testing.NewUserRealm(owner))
815	registerUpgrades(cur)
816	Schedule(cross(cur), "upgrades", "**ready** _now_", MinDelayFloor)
817
818	out := Render("")
819	uassert.False(t, strings.Contains(out, "**ready** _now_"),
820		"emphasis markers in Data must be sanitized")
821}
822
823// ---------- render-index bounds (fix Y4) ----------
824
825func TestRenderIndexSelfTrims(cur realm, t *testing.T) {
826	resetState()
827	testing.SetRealm(testing.NewUserRealm(owner))
828	RegisterTarget(cross(cur), "churn", MinDelayFloor, "")
829
830	// execute many actions so actionIDs would grow unboundedly without
831	// the trim; the index must stay within RenderIndexCap and old
832	// executed records must stay queryable via the map reads
833	var first string
834	for i := 0; i < RenderIndexCap+30; i++ {
835		id := Schedule(cross(cur), "churn", "op", MinDelayFloor)
836		if i == 0 {
837			first = id
838		}
839		skipSeconds(MinDelayFloor)
840		Execute(cross(cur), id)
841	}
842	uassert.True(t, len(actionIDs) <= RenderIndexCap, "render index must self-trim")
843	uassert.True(t, IsExecuted(first), "trimmed records stay queryable forever")
844	uassert.True(t, strings.Contains(GetAction(first), "Status: executed"),
845		"GetAction must still serve trimmed records")
846}
847
848// ---------- round-2 audit regressions ----------
849
850func TestSetGuardian_SweepsExpiredFirst(cur realm, t *testing.T) {
851	resetState()
852	testing.SetRealm(testing.NewUserRealm(owner))
853	registerUpgrades(cur)
854	Schedule(cross(cur), "upgrades", "op", MinDelayFloor)
855	skipSeconds(MinDelayFloor + GracePeriod + 10)
856
857	// round-2 hardening: expired-but-unswept actions protect nothing
858	// and must not block a guardian rotation
859	SetGuardian(cross(cur), "upgrades", "")
860	uassert.Equal(t, "", string(targets["upgrades"].Guardian))
861	uassert.Equal(t, 0, pendingCount("upgrades"), "the gate sweep must reap expired actions")
862}
863
864func TestRenderShowsPendingDespiteChurn(cur realm, t *testing.T) {
865	resetState()
866	testing.SetRealm(testing.NewUserRealm(owner))
867	RegisterTarget(cross(cur), "vis", MinDelayFloor, "")
868	watched := Schedule(cross(cur), "vis", "watched rug", 2*(MinDelayFloor+5*(RenderIndexCap+30)))
869
870	// round-2 fix Y-2: flood the render index with executed churn; the
871	// live pending action must remain visible for its whole window
872	RegisterTarget(cross(cur), "churn2", MinDelayFloor, "")
873	for i := 0; i < RenderIndexCap+30; i++ {
874		id := Schedule(cross(cur), "churn2", "noise", MinDelayFloor)
875		skipSeconds(MinDelayFloor)
876		Execute(cross(cur), id)
877	}
878
879	out := Render("")
880	uassert.True(t, strings.Contains(out, watched), "a live pending action must survive render-index churn")
881	uassert.True(t, strings.Contains(out, "## Pending (1)"), "pending section from live lists")
882
883	// and state stays consistent for an action trimmed out of the
884	// render index: Cancel still works (absent-id removeID is a no-op)
885	uassert.Equal(t, "cancelled: "+watched, Cancel(cross(cur), watched))
886	uassert.Equal(t, 0, pendingCount("vis"))
887}
888
889func TestPendingTargetsBookkeeping(cur realm, t *testing.T) {
890	resetState()
891	testing.SetRealm(testing.NewUserRealm(owner))
892	RegisterTarget(cross(cur), "one", MinDelayFloor, "")
893	RegisterTarget(cross(cur), "two", MinDelayFloor, "")
894
895	// round-2 fix Y-1: only targets with live pendings are tracked
896	uassert.Equal(t, 0, len(pendingTargets))
897	a1 := Schedule(cross(cur), "one", "a", MinDelayFloor)
898	Schedule(cross(cur), "two", "b", MinDelayFloor)
899	uassert.Equal(t, 2, len(pendingTargets))
900
901	Cancel(cross(cur), a1)
902	uassert.Equal(t, 1, len(pendingTargets), "emptied target leaves the with-pendings list")
903	uassert.Equal(t, 1, len(ptIndex))
904
905	// registration alone never inflates the list
906	RegisterTarget(cross(cur), "three", MinDelayFloor, "")
907	uassert.Equal(t, 1, len(pendingTargets))
908}