package timelock_guardian import ( "chain" "strings" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) var ( owner = testutils.TestAddress("owner") guardian = testutils.TestAddress("guardian") attacker = testutils.TestAddress("attacker") newOwner = testutils.TestAddress("newowner") ) // resetState resets all package-level state between tests. func resetState() { targets = make(map[string]*TargetConfig) targetNames = []string{} ownerTargets = make(map[address]int) actions = make(map[string]*Action) actionIDs = []string{} pending = make(map[string][]string) pendingTargets = []string{} ptIndex = make(map[string]int) nextID = 1 } // heights advancing time: SkipHeights moves the clock 5s per height. func skipSeconds(sec int64) { testing.SkipHeights((sec + 4) / 5) } func registerUpgrades(cur realm) string { testing.SetRealm(testing.NewUserRealm(owner)) RegisterTarget(cross(cur), "upgrades", MinDelayFloor, guardian) return "upgrades" } // ---------- RegisterTarget ---------- func TestRegisterTarget_Basic(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) RegisterTarget(cross(cur), "upgrades", 120, guardian) tc := targets["upgrades"] uassert.Equal(t, string(owner), string(tc.Owner)) uassert.Equal(t, string(guardian), string(tc.Guardian)) uassert.Equal(t, int64(120), tc.MinDelay) } func TestRegisterTarget_NoGuardianAllowed(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) RegisterTarget(cross(cur), "plain", MinDelayFloor, "") uassert.Equal(t, "", string(targets["plain"].Guardian)) } func TestRegisterTarget_DuplicateRejected(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) uassert.AbortsWithMessage(t, cur, "target already registered: upgrades", func() { RegisterTarget(cross(cur), "upgrades", MinDelayFloor, "") }, ) } func TestRegisterTarget_InvalidNameRejected(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) for _, bad := range []string{"", "Upgrades", "has space", strings.Repeat("a", MaxTargetNameLen+1)} { uassert.AbortsContains(t, cur, "target name must be", func() { RegisterTarget(cross(cur), bad, MinDelayFloor, "") }, ) } } func TestRegisterTarget_DelayBoundsEnforced(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) uassert.AbortsContains(t, cur, "minimum delay must be at least", func() { RegisterTarget(cross(cur), "too_fast", MinDelayFloor-1, "") }, ) uassert.AbortsContains(t, cur, "minimum delay exceeds the maximum", func() { RegisterTarget(cross(cur), "too_slow", MaxDelay+1, "") }, ) } func TestRegisterTarget_InvalidGuardianRejected(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) uassert.AbortsWithMessage(t, cur, "invalid guardian address: junk", func() { RegisterTarget(cross(cur), "upgrades", MinDelayFloor, address("junk")) }, ) } // ---------- SetGuardian / two-step TransferTargetOwnership ---------- func TestSetGuardian_OwnerOnly(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "only the target owner can set the guardian", func() { SetGuardian(cross(cur), "upgrades", attacker) }, ) testing.SetRealm(testing.NewUserRealm(owner)) SetGuardian(cross(cur), "upgrades", "") uassert.Equal(t, "", string(targets["upgrades"].Guardian)) } func TestTransferTargetOwnership_TwoStep(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "only the target owner can transfer ownership", func() { TransferTargetOwnership(cross(cur), "upgrades", attacker) }, ) testing.SetRealm(testing.NewUserRealm(owner)) uassert.AbortsWithMessage(t, cur, "invalid new owner address: junk", func() { TransferTargetOwnership(cross(cur), "upgrades", address("junk")) }, ) uassert.AbortsWithMessage(t, cur, "new owner is already the owner", func() { TransferTargetOwnership(cross(cur), "upgrades", owner) }, ) // the offer alone changes nothing TransferTargetOwnership(cross(cur), "upgrades", newOwner) uassert.Equal(t, string(owner), string(targets["upgrades"].Owner)) uassert.Equal(t, string(newOwner), string(targets["upgrades"].PendingOwner)) uassert.Equal(t, 1, ownerTargets[owner]) uassert.Equal(t, 0, ownerTargets[newOwner]) // only the nominee can accept testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "caller is not the pending owner", func() { AcceptTargetOwnership(cross(cur), "upgrades") }, ) testing.SetRealm(testing.NewUserRealm(newOwner)) AcceptTargetOwnership(cross(cur), "upgrades") uassert.Equal(t, string(newOwner), string(targets["upgrades"].Owner)) uassert.Equal(t, "", string(targets["upgrades"].PendingOwner)) uassert.Equal(t, 0, ownerTargets[owner]) uassert.Equal(t, 1, ownerTargets[newOwner]) } func TestTransferTargetOwnership_ClearOffer(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) TransferTargetOwnership(cross(cur), "upgrades", newOwner) TransferTargetOwnership(cross(cur), "upgrades", "") // clear testing.SetRealm(testing.NewUserRealm(newOwner)) uassert.AbortsWithMessage(t, cur, "caller is not the pending owner", func() { AcceptTargetOwnership(cross(cur), "upgrades") }, ) } func TestTransferTargetOwnership_QuotaCheckedAtAccept(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(newOwner)) for i := 0; i < MaxTargetsPerOwner; i++ { RegisterTarget(cross(cur), "t_"+strings.Repeat("x", i+1), MinDelayFloor, "") } testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) TransferTargetOwnership(cross(cur), "upgrades", newOwner) // offer succeeds // ...but accepting past the quota is refused, and ownership is unchanged testing.SetRealm(testing.NewUserRealm(newOwner)) uassert.AbortsWithMessage(t, cur, "accepting would exceed the per-owner target limit", func() { AcceptTargetOwnership(cross(cur), "upgrades") }, ) uassert.Equal(t, string(owner), string(targets["upgrades"].Owner)) } func TestUnsolicitedOfferConsumesNothing(cur realm, t *testing.T) { resetState() victim := testutils.TestAddress("victim") // fix Y5 regression: an attacker offering targets to a victim must // not consume the victim's quota or change any of their state testing.SetRealm(testing.NewUserRealm(attacker)) for i := 0; i < MaxTargetsPerOwner; i++ { name := "junk_" + strings.Repeat("x", i+1) RegisterTarget(cross(cur), name, MinDelayFloor, "") TransferTargetOwnership(cross(cur), name, victim) } uassert.Equal(t, 0, ownerTargets[victim]) // the victim can still register a full quota of their own targets testing.SetRealm(testing.NewUserRealm(victim)) for i := 0; i < MaxTargetsPerOwner; i++ { RegisterTarget(cross(cur), "mine_"+strings.Repeat("y", i+1), MinDelayFloor, "") } } // ---------- Schedule ---------- func TestSchedule_OwnerOnly(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) // an attacker cannot schedule an action against someone else's // target — the core binding the audit found missing testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "only the target owner can schedule actions for upgrades", func() { Schedule(cross(cur), "upgrades", "malicious upgrade", MinDelayFloor) }, ) } func TestSchedule_UnregisteredTargetRejected(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) uassert.AbortsWithMessage(t, cur, "target not found: ghost", func() { Schedule(cross(cur), "ghost", "data", MinDelayFloor) }, ) } func TestSchedule_MinDelayEnforced(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) RegisterTarget(cross(cur), "upgrades", 3600, "") // a delay below the target's registered minimum is rejected — no // more 1-second "timelocks" uassert.AbortsWithMessage(t, cur, "delay below the target's minimum of 3600 seconds", func() { Schedule(cross(cur), "upgrades", "data", 60) }, ) } func TestSchedule_MaxDelayEnforced(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) // overflow regression: a delay past MaxDelay must abort, never wrap // ExecuteAfter into the past uassert.AbortsContains(t, cur, "delay exceeds the maximum", func() { Schedule(cross(cur), "upgrades", "data", MaxDelay+1) }, ) } func TestSchedule_DataValidation(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) uassert.AbortsWithMessage(t, cur, "data must not be empty", func() { Schedule(cross(cur), "upgrades", "", MinDelayFloor) }, ) uassert.AbortsContains(t, cur, "data exceeds", func() { Schedule(cross(cur), "upgrades", strings.Repeat("x", MaxDataLen+1), MinDelayFloor) }, ) } func TestSchedule_PendingCap(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) for i := 0; i < MaxPendingPerTarget; i++ { Schedule(cross(cur), "upgrades", "op", MinDelayFloor) } uassert.AbortsWithMessage(t, cur, "too many pending actions for target upgrades", func() { Schedule(cross(cur), "upgrades", "op", MinDelayFloor) }, ) } func TestSchedule_SetsWindows(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) id := Schedule(cross(cur), "upgrades", "rotate keys", 120) a := actions[id] uassert.Equal(t, int64(120), int64(a.ExecuteAfter.Sub(a.ScheduledAt).Seconds())) uassert.Equal(t, GracePeriod, int64(a.ExpiresAt.Sub(a.ExecuteAfter).Seconds())) } // ---------- Execute ---------- func TestExecute_TooEarlyBlocked(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor) uassert.AbortsContains(t, cur, "too early", func() { Execute(cross(cur), id) }) uassert.False(t, IsReady(id), "must not be ready before the delay") } func TestExecute_ReadyAfterDelay_AnyoneCanExecute(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor) skipSeconds(MinDelayFloor) uassert.True(t, IsReady(id), "must be ready after the delay") // executor identity is deliberately unrestricted testing.SetRealm(testing.NewUserRealm(attacker)) uassert.Equal(t, "executed: "+id, Execute(cross(cur), id)) uassert.True(t, IsExecuted(id), "must record execution") } func TestExecute_TwiceBlocked(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor) skipSeconds(MinDelayFloor) Execute(cross(cur), id) uassert.AbortsWithMessage(t, cur, "action already executed", func() { Execute(cross(cur), id) }) } func TestExecute_ExpiredBlocked(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor) // travel past the grace window: stale actions cannot be sprung later skipSeconds(MinDelayFloor + GracePeriod + 10) uassert.False(t, IsReady(id), "expired action must not be ready") uassert.AbortsContains(t, cur, "action expired", func() { Execute(cross(cur), id) }) uassert.Equal(t, "none", GetPending()) } // ---------- Cancel / Veto ---------- func TestCancel_CurrentOwnerOnly(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor) testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "only the target owner can cancel", func() { Cancel(cross(cur), id) }, ) testing.SetRealm(testing.NewUserRealm(owner)) uassert.Equal(t, "cancelled: "+id, Cancel(cross(cur), id)) _, present := actions[id] uassert.False(t, present, "cancelled action must be reaped from state") // re-audit P3: after an ownership transfer the SCHEDULING creator // loses cancel rights; only the current owner holds them id2 := Schedule(cross(cur), "upgrades", "op2", MinDelayFloor) TransferTargetOwnership(cross(cur), "upgrades", newOwner) testing.SetRealm(testing.NewUserRealm(newOwner)) AcceptTargetOwnership(cross(cur), "upgrades") testing.SetRealm(testing.NewUserRealm(owner)) uassert.AbortsWithMessage(t, cur, "only the target owner can cancel", func() { Cancel(cross(cur), id2) }, ) testing.SetRealm(testing.NewUserRealm(newOwner)) uassert.Equal(t, "cancelled: "+id2, Cancel(cross(cur), id2)) } func TestCancel_ExecutedBlocked(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor) skipSeconds(MinDelayFloor) Execute(cross(cur), id) uassert.AbortsWithMessage(t, cur, "cannot cancel an already-executed action", func() { Cancel(cross(cur), id) }, ) } func TestVeto_GuardianOnly(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor) testing.SetRealm(testing.NewUserRealm(attacker)) uassert.AbortsWithMessage(t, cur, "only the target guardian can veto", func() { Veto(cross(cur), id) }, ) testing.SetRealm(testing.NewUserRealm(guardian)) uassert.Equal(t, "vetoed: "+id, Veto(cross(cur), id)) _, present := actions[id] uassert.False(t, present, "vetoed action must be reaped from state") // a vetoed action can never execute skipSeconds(MinDelayFloor) uassert.AbortsWithMessage(t, cur, "action not found: "+id, func() { Execute(cross(cur), id) }) } func TestVeto_NoGuardianTargetBlocked(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) RegisterTarget(cross(cur), "plain", MinDelayFloor, "") id := Schedule(cross(cur), "plain", "op", MinDelayFloor) testing.SetRealm(testing.NewUserRealm(guardian)) uassert.AbortsWithMessage(t, cur, "target has no guardian: plain", func() { Veto(cross(cur), id) }, ) } // ---------- Expire (fix Y2) ---------- func TestExpire_PermissionlessRecovery(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) // fill the target's quota, then let everything expire var ids []string for i := 0; i < MaxPendingPerTarget; i++ { ids = append(ids, Schedule(cross(cur), "upgrades", "op", MinDelayFloor)) } skipSeconds(MinDelayFloor + GracePeriod + 10) // fix Y2 regression: ANYONE — not just a lucky sweep window — can // reap a provably expired action and free the slot it wedges testing.SetRealm(testing.NewUserRealm(attacker)) uassert.Equal(t, "expired: "+ids[0], Expire(cross(cur), ids[0])) _, present := actions[ids[0]] uassert.False(t, present, "expired action must be reaped from state") // reap the rest as the owner; quota and SetGuardian both recover testing.SetRealm(testing.NewUserRealm(owner)) for _, id := range ids[1:] { Expire(cross(cur), id) } uassert.Equal(t, 0, pendingCount("upgrades")) SetGuardian(cross(cur), "upgrades", "") // no longer blocked Schedule(cross(cur), "upgrades", "fresh", MinDelayFloor) } func TestExpire_RefusesLiveAndExecuted(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) live := Schedule(cross(cur), "upgrades", "live", MinDelayFloor) done := Schedule(cross(cur), "upgrades", "done", MinDelayFloor) skipSeconds(MinDelayFloor) Execute(cross(cur), done) uassert.AbortsWithMessage(t, cur, "action is not expired", func() { Expire(cross(cur), live) }) uassert.AbortsWithMessage(t, cur, "action already executed", func() { Expire(cross(cur), done) }) uassert.AbortsWithMessage(t, cur, "action not found: ghost", func() { Expire(cross(cur), "ghost") }) } // ---------- stray sends (fix Y6) ---------- func TestStraySendRejectedEverywhere(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor) // fix Y6 regression: this realm holds no banker, so an attached // send would strand on the realm address forever — every crossing // function must abort it (the abort reverts the transfer) msg := "this realm does not accept coins" testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: 5}}) uassert.AbortsWithMessage(t, cur, msg, func() { RegisterTarget(cross(cur), "paid", MinDelayFloor, "") }) uassert.AbortsWithMessage(t, cur, msg, func() { Schedule(cross(cur), "upgrades", "op2", MinDelayFloor) }) uassert.AbortsWithMessage(t, cur, msg, func() { Cancel(cross(cur), id) }) uassert.AbortsWithMessage(t, cur, msg, func() { SetGuardian(cross(cur), "upgrades", "") }) uassert.AbortsWithMessage(t, cur, msg, func() { TransferTargetOwnership(cross(cur), "upgrades", newOwner) }) uassert.AbortsWithMessage(t, cur, msg, func() { AcceptTargetOwnership(cross(cur), "upgrades") }) uassert.AbortsWithMessage(t, cur, msg, func() { Veto(cross(cur), id) }) uassert.AbortsWithMessage(t, cur, msg, func() { Expire(cross(cur), id) }) // SkipHeights resets the test context (wiping the send envelope), // so re-arm it before probing the ready-action Execute path skipSeconds(MinDelayFloor) testing.SetRealm(testing.NewUserRealm(owner)) testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: 5}}) uassert.AbortsWithMessage(t, cur, msg, func() { Execute(cross(cur), id) }) testing.SetOriginSend(nil) // without coins the same call succeeds uassert.Equal(t, "executed: "+id, Execute(cross(cur), id)) } // ---------- queries + render ---------- func TestGetPending_ExcludesFinished(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) id1 := Schedule(cross(cur), "upgrades", "op1", MinDelayFloor) id2 := Schedule(cross(cur), "upgrades", "op2", MinDelayFloor) id3 := Schedule(cross(cur), "upgrades", "op3", MinDelayFloor) Cancel(cross(cur), id2) skipSeconds(MinDelayFloor) Execute(cross(cur), id1) uassert.Equal(t, id3, GetPending()) } func TestGetPending_GroupedByTarget(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) RegisterTarget(cross(cur), "alpha", MinDelayFloor, "") RegisterTarget(cross(cur), "beta", MinDelayFloor, "") // interleave scheduling across targets; output groups by target in // registration order (fix Y4 bookkeeping), insertion-ordered within a1 := Schedule(cross(cur), "alpha", "a1", MinDelayFloor) b1 := Schedule(cross(cur), "beta", "b1", MinDelayFloor) a2 := Schedule(cross(cur), "alpha", "a2", MinDelayFloor) uassert.Equal(t, a1+", "+a2+", "+b1, GetPending()) uassert.Equal(t, 2, pendingCount("alpha")) uassert.Equal(t, 1, pendingCount("beta")) Cancel(cross(cur), a1) uassert.Equal(t, a2+", "+b1, GetPending()) uassert.Equal(t, 1, pendingCount("alpha")) } func TestGetTarget_Summary(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) out := GetTarget("upgrades") uassert.True(t, strings.Contains(out, string(owner)), "must show owner") uassert.True(t, strings.Contains(out, string(guardian)), "must show guardian") uassert.True(t, strings.Contains(out, "Min delay: 60s"), "must show min delay") TransferTargetOwnership(cross(cur), "upgrades", newOwner) uassert.True(t, strings.Contains(GetTarget("upgrades"), "Pending owner: "+string(newOwner)), "must show a pending ownership offer") } func TestRender_SanitizesInjection(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) Schedule(cross(cur), "upgrades", "evil` | injected\nrow", MinDelayFloor) out := Render("") uassert.False(t, strings.Contains(out, "evil`"), "backticks must be sanitized") uassert.False(t, strings.Contains(out, "injected\nrow"), "newlines must be sanitized") } func TestRender_SanitizesHTMLBrackets(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) // fix Y3 regression: raw HTML must never reach the page Schedule(cross(cur), "upgrades", "", MinDelayFloor) out := Render("") uassert.False(t, strings.Contains(out, "