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

timelock.gno

4.80 Kb · 185 lines
  1// Package timelock is a simplified port of OpenZeppelin's TimelockController.
  2//
  3// Operations are scheduled with a delay (measured in blocks). A scheduled
  4// operation becomes executable only once the chain height reaches the
  5// operation's ready height. Until then it can be cancelled by anyone (this
  6// simplified port omits role-based access control). Once executed or
  7// cancelled, an operation is terminal.
  8//
  9// Solidity mapping:
 10//   - block.number      -> runtime.ChainHeight()
 11//   - msg.sender        -> unsafe.PreviousRealm().Address()
 12//   - require(...)       -> panic(...)
 13//   - events            -> chain.Emit(...)
 14//   - mapping(id => op) -> avl.Tree keyed by zero-padded id
 15package timelock
 16
 17import (
 18	"strconv"
 19
 20	"chain"
 21	"chain/runtime"
 22	"chain/runtime/unsafe"
 23
 24	"gno.land/p/nt/avl/v0"
 25	"gno.land/p/nt/ufmt/v0"
 26)
 27
 28// State labels for an operation.
 29const (
 30	StatePending   = "Pending"   // scheduled, not yet ready
 31	StateReady     = "Ready"     // ready height reached, awaiting execution
 32	StateExecuted  = "Executed"  // terminal
 33	StateCancelled = "Cancelled" // terminal
 34)
 35
 36// Operation is a single scheduled action.
 37type Operation struct {
 38	ID          int
 39	Action      string
 40	Proposer    address
 41	Scheduled   int64 // chain height at scheduling
 42	ReadyHeight int64 // executable once ChainHeight() >= ReadyHeight
 43	Done        bool  // executed
 44	Cancelled   bool
 45}
 46
 47var (
 48	ops    = avl.NewTree() // key: paddedKey(id) -> *Operation
 49	nextID = 1
 50)
 51
 52// Schedule registers a new operation to run after delayBlocks blocks and
 53// returns its id. delayBlocks must be >= 0.
 54func Schedule(cur realm, action string, delayBlocks int) int {
 55	if action == "" {
 56		panic("timelock: empty action")
 57	}
 58	if delayBlocks < 0 {
 59		panic("timelock: negative delay")
 60	}
 61
 62	h := runtime.ChainHeight()
 63	id := nextID
 64	nextID++
 65
 66	op := &Operation{
 67		ID:          id,
 68		Action:      action,
 69		Proposer:    unsafe.PreviousRealm().Address(),
 70		Scheduled:   h,
 71		ReadyHeight: h + int64(delayBlocks),
 72	}
 73	ops.Set(paddedKey(id), op)
 74
 75	chain.Emit("Scheduled",
 76		"id", strconv.Itoa(id),
 77		"action", action,
 78		"readyHeight", strconv.FormatInt(op.ReadyHeight, 10),
 79	)
 80	return id
 81}
 82
 83// Execute runs a ready operation. It panics if the operation is unknown,
 84// already terminal, or not yet ready.
 85func Execute(cur realm, id int) {
 86	op := mustGet(id)
 87	if op.Done {
 88		panic("timelock: already executed")
 89	}
 90	if op.Cancelled {
 91		panic("timelock: operation cancelled")
 92	}
 93	if runtime.ChainHeight() < op.ReadyHeight {
 94		panic("timelock: operation not ready")
 95	}
 96	op.Done = true
 97
 98	chain.Emit("Executed", "id", strconv.Itoa(id), "action", op.Action)
 99}
100
101// Cancel drops a pending or ready operation before it executes. It panics if
102// the operation is unknown or already terminal.
103func Cancel(cur realm, id int) {
104	op := mustGet(id)
105	if op.Done {
106		panic("timelock: already executed")
107	}
108	if op.Cancelled {
109		panic("timelock: already cancelled")
110	}
111	op.Cancelled = true
112
113	chain.Emit("Cancelled", "id", strconv.Itoa(id), "action", op.Action)
114}
115
116// mustGet returns the operation or panics if absent.
117func mustGet(id int) *Operation {
118	v := ops.Get(paddedKey(id))
119	if v == nil {
120		panic("timelock: unknown operation")
121	}
122	return v.(*Operation)
123}
124
125// --- pure/read-only helpers (safe to unit-test) ---
126
127// paddedKey returns a fixed-width, lexicographically sortable key for an id so
128// avl iteration yields operations in id order.
129func paddedKey(id int) string {
130	s := strconv.Itoa(id)
131	for len(s) < 12 {
132		s = "0" + s
133	}
134	return s
135}
136
137// StateOf returns the display state of op at the given chain height.
138func StateOf(op *Operation, height int64) string {
139	switch {
140	case op.Cancelled:
141		return StateCancelled
142	case op.Done:
143		return StateExecuted
144	case height >= op.ReadyHeight:
145		return StateReady
146	default:
147		return StatePending
148	}
149}
150
151// BlocksRemaining returns how many blocks remain before op is ready (0 if
152// already ready or past).
153func BlocksRemaining(op *Operation, height int64) int64 {
154	if height >= op.ReadyHeight {
155		return 0
156	}
157	return op.ReadyHeight - height
158}
159
160// Render implements the standard realm markdown view.
161func Render(path string) string {
162	h := runtime.ChainHeight()
163	out := "# Timelock Controller\n\n"
164	out += ufmt.Sprintf("Current chain height: **%d**\n\n", h)
165
166	if ops.Size() == 0 {
167		out += "_No operations scheduled yet._\n\n"
168		out += "Call `Schedule(action, delayBlocks)` to queue one.\n"
169		return out
170	}
171
172	out += ufmt.Sprintf("Total operations: **%d**\n\n", ops.Size())
173	out += "| ID | Action | State | Ready @ | Blocks left | Proposer |\n"
174	out += "|---:|--------|-------|--------:|------------:|----------|\n"
175
176	ops.Iterate("", "", func(_ string, v any) bool {
177		op := v.(*Operation)
178		state := StateOf(op, h)
179		left := BlocksRemaining(op, h)
180		out += ufmt.Sprintf("| %d | %s | %s | %d | %d | %s |\n",
181			op.ID, op.Action, state, op.ReadyHeight, left, op.Proposer.String())
182		return false
183	})
184	return out
185}