Transaction

7C64883ECAED7A…EAE3B33E2D8D

Block 336,668 · index 0 · indexed

Summary

Hash
7C64883ECAED7A2CE6240F836B36381B4D4D84ADEC51C38BA171EAE3B33E2D8D
Block
336,668
Size
57986 bytes
Gas used
75,859,960 / 250,000,000
Fee
250000 ugnot
Status
success

Messages

Attached funds
12000000 ugnot

Arguments · 9

  1. #1timelock_guardian
  2. #2gnomod.toml
  3. #3module = "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/timelock_guardian" gno = "0.9"
  4. #4timelock_guardian.gno
  5. #5package timelock_guardian import ( "chain" "chain/runtime/unsafe" "strconv" "strings" "time" ) // TargetConfig binds a named target to the only address allowed to // schedule actions against it, an enforced minimum delay, and an // optional guardian who can veto pending actions. Without this binding // a timelock attests nothing: anyone could schedule their own // short-delay action against any name and "execute" it. type TargetConfig struct { Name string Owner address Guardian address // empty = no guardian MinDelay int64 // seconds; every action for this target waits at least this // PendingOwner is the offered-but-not-accepted new owner (fix Y5: // ownership moves in two steps, so a stranger can never have a // target — and its quota slot and pending obligations — dumped on // them without consenting). PendingOwner address } // Action represents a scheduled operation that can only execute after // its delay has elapsed, and only within its grace window. // // State model (re-audit 2026-09-02): only PENDING and EXECUTED actions // are stored. Executed records are permanent attestations consumers // check via IsExecuted. Cancelled, vetoed, and expired actions are // REAPED from state — their history lives in emitted events — so the // live-action cap bounds live exposure and can never be consumed // permanently by schedule/cancel cycling. type Action struct { ID string Target string Creator address Data string // encoded call data or description of the action ScheduledAt time.Time ExecuteAfter time.Time ExpiresAt time.Time // ExecuteAfter + GracePeriod; not executable after Executed bool } const ( // MinDelayFloor is the smallest minimum delay a target may register. // A timelock with a 1-second delay protects nothing. MinDelayFloor = int64(60) // MaxDelay bounds both registered minimum delays and per-action // delays. Also the overflow guard: MaxDelay seconds in nanoseconds // is far below int64 range, so ExecuteAfter arithmetic cannot wrap // into the past. MaxDelay = int64(10 * 365 * 24 * 3600) // 10 years // GracePeriod is how long an action stays executable once ready. // After it, the action expires: something scheduled and forgotten // cannot be sprung on a target years later. GracePeriod = int64(30 * 24 * 3600) // 30 days // Quotas are PER-OWNER only (re-audit round 3): any global cap is a // shared resource a few sybil accounts can exhaust forever (10y // delays defeat expiry sweeping), bricking every other tenant. With // per-owner quotas an attacker only ever consumes their own budget; // state growth is priced in gas and funded accounts. MaxTargetsPerOwner = 10 MaxPendingPerTarget = 20 MaxTargetNameLen = 64 MaxDataLen = 2000 MaxRenderActions = 100 // RenderIndexCap bounds the render-ordering index (fix Y4): the // index self-trims to this size, so reaping an entry from it is a // bounded scan no matter how many actions the realm has ever seen. // Records older than the window stay in state (GetAction/IsExecuted // are map reads and permanent); they only leave the front page. RenderIndexCap = 2 * MaxRenderActions ) var ( targets map[string]*TargetConfig targetNames []string // insertion-ordered for deterministic iteration ownerTargets map[address]int actions map[string]*Action actionIDs []string // insertion-ordered render window, self-trimmed to RenderIndexCap // pending holds each target's LIVE pending action IDs in insertion // order (fix Y4): every list is bounded by MaxPendingPerTarget, so // scans, sweeps, and removals are bounded per call regardless of how // much state OTHER tenants have grown — queue position in a global // list is no longer a shared resource. pending map[string][]string // pendingTargets lists the targets holding >= 1 live pending action, // maintained with O(1) swap-remove via ptIndex (round-2 fix Y-1): no // write path ever scans a list another tenant can grow, and // GetPending/Render iterate only targets that actually have work. pendingTargets []string ptIndex map[string]int nextID int ) func init() { 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 } func now() time.Time { return time.Now() } // ---------- helpers ---------- // rejectStraySend aborts when coins are attached to a call (fix Y6): // this realm handles no funds and holds no banker, so an attached send // would strand on the realm address forever. Aborting reverts the // transfer back to the sender. Fails open for realm-routed calls, whose // attached send lands on the intermediary realm, never here. func rejectStraySend(cur realm) { if cur.Previous().IsUserCall() && len(unsafe.OriginSend()) > 0 { panic("this realm does not accept coins") } } func mustGetTarget(name string) *TargetConfig { t, ok := targets[name] if !ok { panic("target not found: " + name) } return t } func mustGet(id string) *Action { a, ok := actions[id] if !ok { panic("action not found: " + id) } return a } func isValidName(name string) bool { if name == "" || len(name) > MaxTargetNameLen { return false } for _, c := range name { if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') { return false } } return true } func expired(a *Action) bool { return !a.Executed && now().After(a.ExpiresAt) } func removeID(list []string, id string) []string { for i, x := range list { if x == id { return append(list[:i], list[i+1:]...) } } return list } func addPendingTarget(target string) { if _, ok := ptIndex[target]; ok { return } ptIndex[target] = len(pendingTargets) pendingTargets = append(pendingTargets, target) } // removePendingTarget drops a target from the with-pendings list in // O(1) by swapping the last entry into its slot. Order afterwards is // deterministic (a pure function of the operation history), which is // all iteration needs. func removePendingTarget(target string) { i, ok := ptIndex[target] if !ok { return } last := len(pendingTargets) - 1 moved := pendingTargets[last] pendingTargets[i] = moved ptIndex[moved] = i pendingTargets = pendingTargets[:last] delete(ptIndex, target) } // dropPending removes id from its target's pending list. Bounded: the // list never exceeds MaxPendingPerTarget. func dropPending(target, id string) { l := removeID(pending[target], id) if len(l) == 0 { delete(pending, target) removePendingTarget(target) } else { pending[target] = l } } // reap removes a non-executed action from state entirely; its history // is the emitted event. Executed records are permanent attestations: // reap refuses them outright (round-2 hardening), so no future caller // can erase one by mistake. func reap(id string) { a := actions[id] if a != nil && a.Executed { return } if a != nil { dropPending(a.Target, id) } delete(actions, id) actionIDs = removeID(actionIDs, id) } // sweepExpired reaps the expired pending actions of ONE target (fix // Y4/Y2: the old global budget-windowed sweep let long-delay entries at // the front of a shared queue starve everything behind them). A // target's list is bounded by MaxPendingPerTarget, so the sweep is a // bounded scan. Expiry history is the emitted event. func sweepExpired(target string) { var live []string for _, id := range pending[target] { a := actions[id] if a == nil { continue } if expired(a) { chain.Emit("timelock_expired", "id", id, "target", a.Target) delete(actions, id) actionIDs = removeID(actionIDs, id) continue } live = append(live, id) } if len(live) == 0 { delete(pending, target) removePendingTarget(target) } else { pending[target] = live } } // pendingCount is the length of the target's live list. The counter it // replaces (fix Y4) could desync from the lists it mirrored; a length // cannot. It may briefly include not-yet-swept expired actions; those // are reaped by the next Schedule's sweep or by anyone's Expire, and in // the worst case an owner briefly under-uses their own quota — never // another tenant's. func pendingCount(target string) int { return len(pending[target]) } // sanitize makes attacker-controlled text safe to embed in markdown and // single-line summaries: backticks, pipes, newlines, link syntax, and // raw HTML brackets (fix Y3: gnoweb has no HTML sanitization layer, so // a literal <script> tag must never reach the page) are replaced so a // Data string cannot break out of its cell, inject rows or markup, or // render a live link. func sanitize(s string) string { r := strings.NewReplacer( "`", "'", "|", "/", "\n", " ", "\r", " ", "[", "(", "]", ")", "*", "·", "_", "-", "<", "(", ">", ")", ) return r.Replace(s) } // truncate shortens s to at most n runes without splitting a multibyte // character. n is clamped to a minimum of 4 (re-audit: n<=2 sliced out // of bounds). func truncate(s string, n int) string { if n < 4 { n = 4 } runes := []rune(s) if len(runes) <= n { return s } return string(runes[:n-3]) + "..." } // ---------- target registration ---------- // RegisterTarget creates a named target. The caller becomes its owner — // the only address that may schedule actions against it. minDelay is // the enforced floor for every action's delay. guardian may be empty // (no guardian) or an address empowered to veto pending actions. func RegisterTarget(cur realm, name string, minDelay int64, guardian address) { rejectStraySend(cur) owner := cur.Previous().Address() if !isValidName(name) { panic("target name must be 1-" + strconv.Itoa(MaxTargetNameLen) + " chars, lowercase alphanumeric with underscores only") } if _, exists := targets[name]; exists { panic("target already registered: " + name) } if ownerTargets[owner] >= MaxTargetsPerOwner { panic("per-owner target limit reached") } if minDelay < MinDelayFloor { panic("minimum delay must be at least " + strconv.FormatInt(MinDelayFloor, 10) + " seconds") } if minDelay > MaxDelay { panic("minimum delay exceeds the maximum of " + strconv.FormatInt(MaxDelay, 10) + " seconds") } if guardian != "" && !guardian.IsValid() { panic("invalid guardian address: " + string(guardian)) } targets[name] = &TargetConfig{ Name: name, Owner: owner, Guardian: guardian, MinDelay: minDelay, } targetNames = append(targetNames, name) ownerTargets[owner]++ chain.Emit("timelock_target_registered", "target", name, "owner", string(owner), "guardian", string(guardian), "min_delay", strconv.FormatInt(minDelay, 10)) } // SetGuardian changes (or clears, with "") the target's guardian. // Owner only, and REFUSED while the target has pending actions: the // guardian's veto power exists precisely to check the owner during a // delay window, so the owner must not be able to strip it mid-window. // // KNOWN LIMIT (documented, round-3 audit): the owner can cancel all // pending actions, change the guardian, and reschedule — the price is // a full fresh MinDelay on every rescheduled action, and every step // emits an event (cancellations + the guardian change below), so // observers always get MinDelay of warning under the new guardian // regime. Guardians protect open windows, not the owner's future. func SetGuardian(cur realm, targetName string, guardian address) { rejectStraySend(cur) t := mustGetTarget(targetName) if cur.Previous().Address() != t.Owner { panic("only the target owner can set the guardian") } // expired-but-unswept actions protect nothing; sweep them so they // cannot block a guardian rotation (round-2 hardening) sweepExpired(targetName) if pendingCount(targetName) > 0 { panic("cannot change the guardian while actions are pending") } if guardian != "" && !guardian.IsValid() { panic("invalid guardian address: " + string(guardian)) } t.Guardian = guardian chain.Emit("timelock_guardian_changed", "target", targetName, "guardian", string(guardian)) } // TransferTargetOwnership OFFERS a target to a new owner; the nominee // must AcceptTargetOwnership to complete it (fix Y5: a one-step // transfer let anyone fill a stranger's per-owner quota and dump // pending obligations — with an attacker-chosen guardian — on an // address that never asked). Owner only. Pass "" to clear a pending // offer. Nothing changes hands until the nominee accepts. func TransferTargetOwnership(cur realm, targetName string, newOwner address) { rejectStraySend(cur) t := mustGetTarget(targetName) if cur.Previous().Address() != t.Owner { panic("only the target owner can transfer ownership") } if newOwner == "" { t.PendingOwner = "" chain.Emit("timelock_ownership_offer_cleared", "target", targetName) return } if !newOwner.IsValid() { panic("invalid new owner address: " + string(newOwner)) } if newOwner == t.Owner { panic("new owner is already the owner") } t.PendingOwner = newOwner chain.Emit("timelock_ownership_offered", "target", targetName, "pending_owner", string(newOwner)) } // AcceptTargetOwnership completes a pending ownership offer; only the // nominee can accept. The nominee's quota is checked HERE — consent // time — so an offer can never overfill an account that did not agree // to carry it. func AcceptTargetOwnership(cur realm, targetName string) { rejectStraySend(cur) t := mustGetTarget(targetName) a := cur.Previous().Address() if t.PendingOwner == "" || a != t.PendingOwner { panic("caller is not the pending owner") } if ownerTargets[a] >= MaxTargetsPerOwner { panic("accepting would exceed the per-owner target limit") } ownerTargets[t.Owner]-- if ownerTargets[t.Owner] <= 0 { delete(ownerTargets, t.Owner) } ownerTargets[a]++ t.Owner = a t.PendingOwner = "" chain.Emit("timelock_ownership_transferred", "target", targetName, "new_owner", string(a)) } // ---------- write operations ---------- // Schedule creates a new timelocked action against a registered target. // Only the target's owner may schedule. The delay must be at least the // target's registered minimum and at most MaxDelay. Returns the action ID. func Schedule(cur realm, targetName, data string, delay int64) string { rejectStraySend(cur) t := mustGetTarget(targetName) caller := cur.Previous().Address() if caller != t.Owner { panic("only the target owner can schedule actions for " + targetName) } if data == "" { panic("data must not be empty") } if len(data) > MaxDataLen { panic("data exceeds " + strconv.Itoa(MaxDataLen) + " bytes") } if delay < t.MinDelay { panic("delay below the target's minimum of " + strconv.FormatInt(t.MinDelay, 10) + " seconds") } if delay > MaxDelay { panic("delay exceeds the maximum of " + strconv.FormatInt(MaxDelay, 10) + " seconds") } sweepExpired(targetName) if pendingCount(targetName) >= MaxPendingPerTarget { panic("too many pending actions for target " + targetName) } id := "action_" + strconv.Itoa(nextID) nextID++ ts := now() execAfter := ts.Add(time.Duration(delay) * time.Second) actions[id] = &Action{ ID: id, Target: targetName, Creator: caller, Data: data, ScheduledAt: ts, ExecuteAfter: execAfter, ExpiresAt: execAfter.Add(time.Duration(GracePeriod) * time.Second), } actionIDs = append(actionIDs, id) // self-trim (fix Y4): the render index stays bounded, so reaping an // entry from it can never become an unbounded scan. Trimmed entries // remain in the actions map; only front-page ordering forgets them. if len(actionIDs) > RenderIndexCap { actionIDs = append([]string{}, actionIDs[len(actionIDs)-MaxRenderActions:]...) } pending[targetName] = append(pending[targetName], id) addPendingTarget(targetName) // full-attribute event (round-3 audit): reaped records must be // reconstructible from events alone chain.Emit("timelock_scheduled", "id", id, "target", targetName, "creator", string(caller), "data", data, "delay", strconv.FormatInt(delay, 10), "execute_after", execAfter.Format(time.RFC3339), "expires_at", execAfter.Add(time.Duration(GracePeriod)*time.Second).Format(time.RFC3339)) return id } // Execute marks an action as executed. Anyone can call this — the // timelock is the protection, not the executor's identity. The action // must exist, be pending, its delay elapsed, and its grace window not // yet expired. func Execute(cur realm, actionID string) string { rejectStraySend(cur) a := mustGet(actionID) if a.Executed { panic("action already executed") } if now().Before(a.ExecuteAfter) { remaining := a.ExecuteAfter.Sub(now()) panic("too early: " + strconv.FormatInt(int64(remaining/time.Second), 10) + "s remaining") } if now().After(a.ExpiresAt) { panic("action expired: the grace window of " + strconv.FormatInt(GracePeriod, 10) + "s after readiness has passed") } a.Executed = true dropPending(a.Target, actionID) chain.Emit("timelock_executed", "id", actionID, "target", a.Target) return "executed: " + actionID } // Cancel removes a pending action. Only the target's CURRENT owner can // cancel (re-audit: the scheduling creator's rights must not survive an // ownership transfer). The record is reaped; history is the event. func Cancel(cur realm, actionID string) string { rejectStraySend(cur) a := mustGet(actionID) t := mustGetTarget(a.Target) if cur.Previous().Address() != t.Owner { panic("only the target owner can cancel") } if a.Executed { panic("cannot cancel an already-executed action") } if expired(a) { panic("action expired: nothing to cancel (use Expire)") } chain.Emit("timelock_cancelled", "id", actionID, "target", a.Target) reap(actionID) return "cancelled: " + actionID } // Veto cancels a pending action as the target's guardian. This is the // guardian's whole power: it can stop a scheduled action during the // delay window, never create or execute one. The record is reaped; // history is the event. func Veto(cur realm, actionID string) string { rejectStraySend(cur) a := mustGet(actionID) t := mustGetTarget(a.Target) if t.Guardian == "" { panic("target has no guardian: " + a.Target) } if cur.Previous().Address() != t.Guardian { panic("only the target guardian can veto") } if a.Executed { panic("cannot veto an already-executed action") } if expired(a) { panic("action expired: nothing to veto (use Expire)") } chain.Emit("timelock_vetoed", "id", actionID, "target", a.Target) reap(actionID) return "vetoed: " + actionID } // Expire reaps a provably expired action. Permissionless (fix Y2): an // expired action decides nothing — reaping it only writes down what the // clock already decided — so anyone may free the quota slot it holds. // This is the recovery valve that makes a wedged target impossible: // before it, a phantom expired entry consumed quota and blocked // SetGuardian until a global sweep happened to reach it; now its own // target's owner — or anyone else — reaps it directly. func Expire(cur realm, actionID string) string { rejectStraySend(cur) a := mustGet(actionID) if a.Executed { panic("action already executed") } if !expired(a) { panic("action is not expired") } chain.Emit("timelock_expired", "id", actionID, "target", a.Target) reap(actionID) return "expired: " + actionID } // ---------- read-only queries ---------- // GetTarget returns a formatted summary of a registered target. func GetTarget(targetName string) string { t := mustGetTarget(targetName) var b strings.Builder b.WriteString("Target: " + t.Name + "\n") b.WriteString("Owner: " + string(t.Owner) + "\n") guardian := "none" if t.Guardian != "" { guardian = string(t.Guardian) } b.WriteString("Guardian: " + guardian + "\n") b.WriteString("Min delay: " + strconv.FormatInt(t.MinDelay, 10) + "s\n") if t.PendingOwner != "" { b.WriteString("Pending owner: " + string(t.PendingOwner) + "\n") } return b.String() } // GetAction returns a formatted summary of a single pending or executed // action. Cancelled/vetoed/expired actions are reaped — their history // is in emitted events. func GetAction(actionID string) string { a := mustGet(actionID) return formatAction(a) } // GetPending returns the IDs of all pending, non-expired actions, // grouped by target, insertion-ordered within a target. Only targets // that actually hold live pendings are visited (round-2 fix Y-1), so // the scan cannot be inflated by registrations alone. func GetPending() string { var results []string for _, name := range pendingTargets { for _, id := range pending[name] { a := actions[id] if a == nil || a.Executed || expired(a) { continue } results = append(results, a.ID) } } if len(results) == 0 { return "none" } return strings.Join(results, ", ") } // IsReady returns true if the action exists, is pending, its delay has // elapsed, and it has not expired. func IsReady(actionID string) bool { a, ok := actions[actionID] if !ok || a.Executed || expired(a) { return false } return !now().Before(a.ExecuteAfter) } // IsExecuted returns true if the action exists and was executed. This is // the consumer-side check: combined with target registration it attests // that the target's registered owner scheduled the action, it waited at // least the registered minimum delay, no guardian vetoed it, and it was // executed within its grace window. Executed records are permanent. func IsExecuted(actionID string) bool { a, ok := actions[actionID] return ok && a.Executed } // ---------- render ---------- // Render returns a markdown overview. Never panics. Cancelled, vetoed, // and expired actions are reaped from state; their history is in // events. The page shows the most recent actions only (the ordering // index is bounded — fix Y4); older executed records stay queryable // via GetAction/IsExecuted forever. func Render(path string) string { if actions == nil || len(actions) == 0 { return "# Timelock Guardian\n\nNo actions scheduled.\n" } // The Pending section reads the live per-target lists, so flooding // the realm with executed churn can never push a still-live pending // action off the page during its delay window (round-2 fix Y-2). var pendingList []*Action truncated := false for _, name := range pendingTargets { for _, id := range pending[name] { a := actions[id] if a == nil || a.Executed || expired(a) { continue } if len(pendingList) >= MaxRenderActions { truncated = true break } pendingList = append(pendingList, a) } if truncated { break } } // The Executed section reads the bounded render index: recent // attestations only; older ones stay queryable via GetAction. var executedList []*Action show := actionIDs if len(show) > MaxRenderActions { show = show[len(show)-MaxRenderActions:] truncated = true } for _, id := range show { a := actions[id] if a == nil || !a.Executed { continue } executedList = append(executedList, a) } var b strings.Builder b.WriteString("# Timelock Guardian\n\n") if truncated { b.WriteString("_Showing at most " + strconv.Itoa(MaxRenderActions) + " actions per section._\n\n") } section := func(title string, list []*Action) { if len(list) == 0 { return } b.WriteString("## " + title + " (" + strconv.Itoa(len(list)) + ")\n\n") for _, a := range list { b.WriteString(renderActionRow(a)) } b.WriteString("\n") } section("Pending", pendingList) section("Executed", executedList) return b.String() } // ---------- formatting helpers ---------- func formatAction(a *Action) string { var b strings.Builder b.WriteString("ID: " + a.ID + "\n") b.WriteString("Target: " + sanitize(a.Target) + "\n") b.WriteString("Creator: " + string(a.Creator) + "\n") b.WriteString("Data: " + sanitize(a.Data) + "\n") b.WriteString("Scheduled: " + a.ScheduledAt.Format(time.RFC3339) + "\n") b.WriteString("Execute after: " + a.ExecuteAfter.Format(time.RFC3339) + "\n") b.WriteString("Expires: " + a.ExpiresAt.Format(time.RFC3339) + "\n") switch { case a.Executed: b.WriteString("Status: executed\n") case expired(a): b.WriteString("Status: expired\n") case now().Before(a.ExecuteAfter): remaining := a.ExecuteAfter.Sub(now()) b.WriteString("Status: locked (" + strconv.FormatInt(int64(remaining/time.Second), 10) + "s remaining)\n") default: b.WriteString("Status: ready\n") } return b.String() } func renderActionRow(a *Action) string { data := truncate(sanitize(a.Data), 40) status := "pending" switch { case a.Executed: status = "executed" case expired(a): status = "expired" case !now().Before(a.ExecuteAfter): status = "**ready**" } return "- **" + a.ID + "** | target: `" + sanitize(a.Target) + "` | " + data + " | " + status + "\n" }
  6. #6timelock_guardian_test.gno
  7. #7package 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", "<script>alert(1)</script>", MinDelayFloor) out := Render("") uassert.False(t, strings.Contains(out, "<script"), "script tags must be sanitized in Render") uassert.False(t, strings.Contains(out, "</script"), "closing tags must be sanitized in Render") one := GetAction(GetPending()) uassert.False(t, strings.Contains(one, "<script"), "script tags must be sanitized in GetAction") } func TestRender_EmptyState(cur realm, t *testing.T) { resetState() uassert.Equal(t, "# Timelock Guardian\n\nNo actions scheduled.\n", Render("")) } func TestRender_Sections(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) Schedule(cross(cur), "upgrades", "op3", MinDelayFloor) skipSeconds(MinDelayFloor) Execute(cross(cur), id1) Cancel(cross(cur), id2) out := Render("") uassert.True(t, strings.Contains(out, "## Executed (1)"), "executed section") uassert.True(t, strings.Contains(out, "## Pending (1)"), "pending section") uassert.False(t, strings.Contains(out, "Cancelled"), "cancelled actions are reaped, not rendered") } // ---------- re-audit regressions (2026-09-02) ---------- func TestScheduleCancelCycling_CannotBrickTheRealm(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) // re-audit P1: the old lifetime cap could be consumed forever by // schedule+cancel cycling; the cap now bounds LIVE actions and // cancelled records are reaped, so heavy cycling leaves no residue for i := 0; i < 3*MaxPendingPerTarget; i++ { id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor) Cancel(cross(cur), id) } uassert.Equal(t, 0, len(actions), "cycling must leave no state residue") uassert.Equal(t, 0, pendingCount("upgrades"), "no live actions after cycling") Schedule(cross(cur), "upgrades", "op", MinDelayFloor) // must not abort } func TestSetGuardian_BlockedWhilePending(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) id := Schedule(cross(cur), "upgrades", "rug", MinDelayFloor) // re-audit P2: the owner must not be able to strip the guardian // during the delay window the guardian exists to police uassert.AbortsWithMessage(t, cur, "cannot change the guardian while actions are pending", func() { SetGuardian(cross(cur), "upgrades", "") }, ) // once nothing is pending, the owner may change the guardian Cancel(cross(cur), id) SetGuardian(cross(cur), "upgrades", "") uassert.Equal(t, "", string(targets["upgrades"].Guardian)) } func TestVetoAndCancel_RefuseExpired(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) id := Schedule(cross(cur), "upgrades", "op", MinDelayFloor) skipSeconds(MinDelayFloor + GracePeriod + 10) // re-audit P3: terminal-state writes on expired actions rewrote // history; both must refuse and point at the Expire valve uassert.AbortsWithMessage(t, cur, "action expired: nothing to cancel (use Expire)", func() { Cancel(cross(cur), id) }) testing.SetRealm(testing.NewUserRealm(guardian)) uassert.AbortsWithMessage(t, cur, "action expired: nothing to veto (use Expire)", func() { Veto(cross(cur), id) }) } func TestExpiredActions_ReapedOnSchedule(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) Schedule(cross(cur), "upgrades", "op", MinDelayFloor) skipSeconds(MinDelayFloor + GracePeriod + 10) // the next Schedule sweeps expired actions out of state Schedule(cross(cur), "upgrades", "op2", MinDelayFloor) uassert.Equal(t, 1, len(actions), "expired action must be reaped by the sweep") } func TestRender_LinkInjectionSanitized(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) Schedule(cross(cur), "upgrades", "[approve](https://evil.example)", MinDelayFloor) out := Render("") uassert.False(t, strings.Contains(out, "[approve]"), "link syntax must be sanitized") } func TestPerOwnerTargetCap(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) for i := 0; i < MaxTargetsPerOwner; i++ { RegisterTarget(cross(cur), "t_"+strings.Repeat("x", i+1), MinDelayFloor, "") } uassert.AbortsWithMessage(t, cur, "per-owner target limit reached", func() { RegisterTarget(cross(cur), "one_more", MinDelayFloor, "") }, ) } // ---------- round-3 audit regressions (2026-09-02) ---------- func TestSybilsCannotExhaustSharedCapacity(cur realm, t *testing.T) { resetState() // round-3 P1: two attackers filling their OWN per-owner quotas must // never block a third owner's Schedule — there is no shared cap left // to exhaust sybils := []address{testutils.TestAddress("sybil_a"), testutils.TestAddress("sybil_b")} for _, syb := range sybils { testing.SetRealm(testing.NewUserRealm(syb)) for i := 0; i < MaxTargetsPerOwner; i++ { name := "t_" + string(syb[len(syb)-4:]) + "_" + strings.Repeat("x", i+1) RegisterTarget(cross(cur), name, MinDelayFloor, "") for j := 0; j < MaxPendingPerTarget; j++ { Schedule(cross(cur), name, "spam", MaxDelay) // 10y delay: sweep reclaims nothing } } } // a legitimate owner is entirely unaffected testing.SetRealm(testing.NewUserRealm(owner)) RegisterTarget(cross(cur), "legit", MinDelayFloor, "") Schedule(cross(cur), "legit", "real work", MinDelayFloor) // must not abort } func TestVictimRecoversBehindSybilWall(cur realm, t *testing.T) { resetState() // fix Y2/Y4 regression — the exact starvation case the old global // budget-windowed sweep could not recover from: a wall of long-delay // sybil actions in front, a victim whose actions expired behind it. // Per-target sweeps and permissionless Expire make recovery local. syb := testutils.TestAddress("wall") testing.SetRealm(testing.NewUserRealm(syb)) for i := 0; i < 5; i++ { name := "wall_" + strings.Repeat("w", i+1) RegisterTarget(cross(cur), name, MinDelayFloor, "") for j := 0; j < MaxPendingPerTarget; j++ { Schedule(cross(cur), name, "wall", MaxDelay) // 100 live 10y entries } } victim := testutils.TestAddress("victim2") testing.SetRealm(testing.NewUserRealm(victim)) RegisterTarget(cross(cur), "vt", MinDelayFloor, guardian) var ids []string for j := 0; j < MaxPendingPerTarget; j++ { ids = append(ids, Schedule(cross(cur), "vt", "op", MinDelayFloor)) } skipSeconds(MinDelayFloor + GracePeriod + 10) // all 20 expire // the victim's own next Schedule sweeps THEIR target despite the wall Schedule(cross(cur), "vt", "recovered", MinDelayFloor) uassert.Equal(t, 1, pendingCount("vt"), "expired actions swept despite the sybil wall") // and Expire frees a wedged slot directly, from any key id2 := Schedule(cross(cur), "vt", "will expire", MinDelayFloor) testing.SetRealm(testing.NewUserRealm(attacker)) skipSeconds(MinDelayFloor + GracePeriod + 10) Expire(cross(cur), id2) _, present := actions[id2] uassert.False(t, present, "Expire must reap regardless of global queue position") } func TestPendingCounterConsistency(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) id1 := Schedule(cross(cur), "upgrades", "a", MinDelayFloor) id2 := Schedule(cross(cur), "upgrades", "b", MinDelayFloor) Schedule(cross(cur), "upgrades", "c", MinDelayFloor) uassert.Equal(t, 3, pendingCount("upgrades")) Cancel(cross(cur), id1) uassert.Equal(t, 2, pendingCount("upgrades")) skipSeconds(MinDelayFloor) Execute(cross(cur), id2) uassert.Equal(t, 1, pendingCount("upgrades")) // expiry: the count drops when the sweep reaps skipSeconds(GracePeriod + 10) Schedule(cross(cur), "upgrades", "d", MinDelayFloor) // triggers sweep of expired "c" uassert.Equal(t, 1, pendingCount("upgrades")) } func TestSanitize_EmphasisMarkers(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) Schedule(cross(cur), "upgrades", "**ready** _now_", MinDelayFloor) out := Render("") uassert.False(t, strings.Contains(out, "**ready** _now_"), "emphasis markers in Data must be sanitized") } // ---------- render-index bounds (fix Y4) ---------- func TestRenderIndexSelfTrims(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) RegisterTarget(cross(cur), "churn", MinDelayFloor, "") // execute many actions so actionIDs would grow unboundedly without // the trim; the index must stay within RenderIndexCap and old // executed records must stay queryable via the map reads var first string for i := 0; i < RenderIndexCap+30; i++ { id := Schedule(cross(cur), "churn", "op", MinDelayFloor) if i == 0 { first = id } skipSeconds(MinDelayFloor) Execute(cross(cur), id) } uassert.True(t, len(actionIDs) <= RenderIndexCap, "render index must self-trim") uassert.True(t, IsExecuted(first), "trimmed records stay queryable forever") uassert.True(t, strings.Contains(GetAction(first), "Status: executed"), "GetAction must still serve trimmed records") } // ---------- round-2 audit regressions ---------- func TestSetGuardian_SweepsExpiredFirst(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) registerUpgrades(cur) Schedule(cross(cur), "upgrades", "op", MinDelayFloor) skipSeconds(MinDelayFloor + GracePeriod + 10) // round-2 hardening: expired-but-unswept actions protect nothing // and must not block a guardian rotation SetGuardian(cross(cur), "upgrades", "") uassert.Equal(t, "", string(targets["upgrades"].Guardian)) uassert.Equal(t, 0, pendingCount("upgrades"), "the gate sweep must reap expired actions") } func TestRenderShowsPendingDespiteChurn(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) RegisterTarget(cross(cur), "vis", MinDelayFloor, "") watched := Schedule(cross(cur), "vis", "watched rug", 2*(MinDelayFloor+5*(RenderIndexCap+30))) // round-2 fix Y-2: flood the render index with executed churn; the // live pending action must remain visible for its whole window RegisterTarget(cross(cur), "churn2", MinDelayFloor, "") for i := 0; i < RenderIndexCap+30; i++ { id := Schedule(cross(cur), "churn2", "noise", MinDelayFloor) skipSeconds(MinDelayFloor) Execute(cross(cur), id) } out := Render("") uassert.True(t, strings.Contains(out, watched), "a live pending action must survive render-index churn") uassert.True(t, strings.Contains(out, "## Pending (1)"), "pending section from live lists") // and state stays consistent for an action trimmed out of the // render index: Cancel still works (absent-id removeID is a no-op) uassert.Equal(t, "cancelled: "+watched, Cancel(cross(cur), watched)) uassert.Equal(t, 0, pendingCount("vis")) } func TestPendingTargetsBookkeeping(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(owner)) RegisterTarget(cross(cur), "one", MinDelayFloor, "") RegisterTarget(cross(cur), "two", MinDelayFloor, "") // round-2 fix Y-1: only targets with live pendings are tracked uassert.Equal(t, 0, len(pendingTargets)) a1 := Schedule(cross(cur), "one", "a", MinDelayFloor) Schedule(cross(cur), "two", "b", MinDelayFloor) uassert.Equal(t, 2, len(pendingTargets)) Cancel(cross(cur), a1) uassert.Equal(t, 1, len(pendingTargets), "emptied target leaves the with-pendings list") uassert.Equal(t, 1, len(ptIndex)) // registration alone never inflates the list RegisterTarget(cross(cur), "three", MinDelayFloor, "") uassert.Equal(t, 1, len(pendingTargets)) }
  8. #8/gno.MemPackageType
  9. #9 MPUserAll

Result log

msg:0,success:true,log:,events:[]