Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}