timelock_guardian
gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/timelock_guardian on gno.land mainnet
realmpipeline-applicationsecurity-delayed-executionlive on mainnet
Delay a sensitive action, with time to cancel it.
An action waits out a minimum delay in the open before it can run, and a named guardian can veto it in the meantime.
In detail curated
Delayed-execution guardian: queue, delay window, execute or cancel. GitHub-pipeline application #2.
Use it
A realm holds state. Reading it is a free query; changing it is a transaction you sign yourself.
Read it — no key, no account, no GNOT
gnokey query vm/qeval \
--data 'gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/timelock_guardian.GetPending()' \
--remote https://rpc.gno.land:443GetPending is read-only, so this is a query rather than a transaction: it costs nothing, signs nothing and needs no funded account. Every exported function is listed under the API below; swap the name and the arguments to read any of the others.
Change its state
Every state change is a transaction you sign with your own key — this site never asks for one and holds none. You do not have to hand-write the transaction: gnoweb generates the exact gnokey maketx call command for any of this realm's 9 crossing functions, with the argument names filled in.
The gno.land mainnet builder spends real GNOT. Start with the read above, and size -gas-fee from a simulation rather than from any example.
Read the known limitations further down this page first. They record what was not verified on chain, which is usually the part that matters.
Import it from another realm
import "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/timelock_guardian"The path is the address: there is no package manager, nothing to add to a manifest and no version to pin, so the package you compile against is the one the chain runs. It differs per chain because the namespace in it is the deploying account's.
Called from real code
Not an example written for this page. These lines are quoted from upkeep, a deployed package in this portfolio that imports this one as guardian, out of the committed file whose sha256 gno.land mainnet attested. They compile because they are compiled.
guardian.Execute(cross(cur), actionID)Deployments
gno.land mainnet gnoland-1
RenderedSourceCall builderState
chain-attested| Import path | gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/timelock_guardian |
|---|---|
| Namespace | g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39 |
| Deployed at height | 336,668 |
| Deploy transaction | 7c64883ecaed7a2ce6240f836b36381b4d4d84adec51c38ba171eae3b33e2d8d look it up on the RPC |
| Deployer | g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39 |
| Gas used | 75,859,960 |
| Storage | not attested per package on this chain — see the limitations below |
| Files on chain | gnomod.toml timelock_guardian.gno timelock_guardian_test.gno |
| Deployed bytes | timelock_guardian.gno — 24,983 bytes |
| sha256 | 72ca7049496257e6fb00216aa027736fa90d41c1dbc91cf8059fc8fd29080f07 |
| Realm address | g1nlrwhuatzwqggak4wld5jfn6keztwt3atfnnq9 derived, never confirmed against the realm |
Do not take the hash above on trust. $download returns the bytes gnoland-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:
curl -sS 'https://gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/timelock_guardian$download&file=timelock_guardian.gno' | shasum -a 256Expected: 72ca7049496257e6fb00216aa027736fa90d41c1dbc91cf8059fc8fd29080f07 — 24,983 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.
API
chain-derived 15 exported functions, 2 types.
Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:
AcceptTargetOwnershipCancelExecuteExpireGetActionGetPendingGetTargetIsExecutedIsReadyRegisterTargetRenderScheduleSetGuardianTransferTargetOwnershipVeto
Overview
This package carries no package doc comment on chain, so there is nothing for vm/qdoc to return and gnoweb's $help Overview is empty. Deployed bytes are immutable, so this cannot be repaired in place — see [an unpublished record] §2.4.
Imports
chainchain/runtime/unsafestrconvstringstime
Constants and variables
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
)
Types
type Action
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
}
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.
| Exported field | Type | Doc |
|---|---|---|
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 |
type TargetConfig
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
}
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.
| Exported field | Type | Doc |
|---|---|---|
Name | string | |
Owner | address | |
Guardian | address | empty = no guardian |
MinDelay | int64 | seconds; every action for this target waits at least this |
PendingOwner | address | 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). |
Functions
AcceptTargetOwnership
func AcceptTargetOwnership(cur realm, targetName string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asAcceptTargetOwnership(cross(cur), ...).
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.
Cancel
func Cancel(cur realm, actionID string) string
Crossing function. Callable from a transaction via
MsgCall, and from another realm asCancel(cross(cur), ...).
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.
Execute
func Execute(cur realm, actionID string) string
Crossing function. Callable from a transaction via
MsgCall, and from another realm asExecute(cross(cur), ...).
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.
Expire
func Expire(cur realm, actionID string) string
Crossing function. Callable from a transaction via
MsgCall, and from another realm asExpire(cross(cur), ...).
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.
GetAction
func GetAction(actionID string) 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.
GetPending
func GetPending() string
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.
GetTarget
func GetTarget(targetName string) string
GetTarget returns a formatted summary of a registered target.
IsExecuted
func IsExecuted(actionID string) bool
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.
IsReady
func IsReady(actionID string) bool
IsReady returns true if the action exists, is pending, its delay has elapsed, and it has not expired.
RegisterTarget
func RegisterTarget(cur realm, name string, minDelay int64, guardian address)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asRegisterTarget(cross(cur), ...).
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.
Render
func Render(path string) string
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.
Schedule
func Schedule(cur realm, targetName, data string, delay int64) string
Crossing function. Callable from a transaction via
MsgCall, and from another realm asSchedule(cross(cur), ...).
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.
SetGuardian
func SetGuardian(cur realm, targetName string, guardian address)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asSetGuardian(cross(cur), ...).
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.
TransferTargetOwnership
func TransferTargetOwnership(cur realm, targetName string, newOwner address)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asTransferTargetOwnership(cross(cur), ...).
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.
Veto
func Veto(cur realm, actionID string) string
Crossing function. Callable from a transaction via
MsgCall, and from another realm asVeto(cross(cur), ...).
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.
Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.
Dependencies
chain-attested| Imports | chain, chain/runtime/unsafe, strconv, strings, time |
|---|---|
| First-party dependencies | none |
Known limitations
Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.
curated- No state-changing call was made against this realm on gnoland-1. The live evidence is byte custody plus a successful vm/qrender; every state transition rests on the committed test suite and the pearl-1 audit, not on mainnet behaviour ([an unpublished record], Live verification).
- storage_bytes and storage_deposit are null because gnoland-1 emits no /tm.StorageDepositEvent ON ADDPKG -- a scope that is load-bearing, because it DOES emit one on MsgCall (measured 2026-10-17, AL62); see the chain's storage_deposit_events field. The deposit actually charged was 3,734,200 ugnot, measured from the signer's balance and recorded in [an unpublished record], Cost; it is not chain-attested per package by this manifest.
- The deployed file set includes timelock_guardian_test.gno. The pearl-1 deployment of the same realm carried only the source and gnomod.toml, so the mainnet package is three files where pearl's is two -- the test file is stored on chain and paid for in the deposit. It declares no crossing function and so adds no callable surface, but it is part of the immutable package.
Source
| Source file | mainnet/r/timelock_guardian/timelock_guardian.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository) |
|---|---|
| Matches the deployed bytes | yes — byte-identical |