diff --git a/content/contracts-sui/1.x/api/timelock.mdx b/content/contracts-sui/1.x/api/timelock.mdx new file mode 100644 index 00000000..714b70cd --- /dev/null +++ b/content/contracts-sui/1.x/api/timelock.mdx @@ -0,0 +1,828 @@ +--- +title: Timelock API Reference +--- + +This page documents the public API of `openzeppelin_timelock` for OpenZeppelin Contracts for Sui `v1.x`. + +### `timelock` [toc] [#timelock] + + + +```move +use openzeppelin_timelock::timelock; +``` + +Hash-keyed operation timelock with typed, on-chain operation parameters and a typed hot-potato execution ticket. It is a Sui-native take on OpenZeppelin's [`TimelockController`](https://docs.openzeppelin.com/contracts/5.x/api/governance#TimelockController). A proposer schedules an operation with its typed params stored on-chain under a deterministic, off-chain reproducible id. After the per-op delay elapses and before the grace window closes (the ready window is half-open, `[ready_at_ms, expires_at_ms)`, and both bounds are locked at schedule time), an executor executes it, minting an `ExecutionTicket`, a no-ability hot potato that must be consumed by `consume` in the same PTB. `Done` is sticky, so operations are non-replayable. + +Authorization comes from a hard dependency on `openzeppelin_access`: every gated entry takes `&Auth` and asserts `Role` matches one of the four role types bound into the `Timelock` at creation (`proposer_role` / `executor_role` / `canceller_role` / `admin_role`). Role types are immutable; membership is managed in the consumer's `AccessControl`. Configuration (`min_delay_ms`, `grace_period_ms`, `open_executor`) is mutated only through the admin-gated `schedule_* / execute_*` pairs, so every configuration change is itself timelocked. + +The raw entries (`schedule` / `execute` / `execute_open` / `cancel`) do NOT bind the call to a specific `Timelock` id. A consumer using them must pin the canonical timelock id itself, or an actor holding the roles can route a self-created zero-delay timelock through the consumer. Prefer the `*_with` entries, which take a stored `OperationCap` that enforces the canonical-timelock binding structurally. `Action` witness types must be `drop`-only, one per consume function, and their construction must never leak across the package boundary. + +Types + +- [`Timelock`](#timelock-Timelock) +- [`OpTimestamp`](#timelock-OpTimestamp) +- [`ExecutionTicket`](#timelock-ExecutionTicket) +- [`OperationCap`](#timelock-OperationCap) +- [`IdInput`](#timelock-IdInput) +- [`OperationState`](#timelock-OperationState) +- [`UpdateMinDelayWitness`](#timelock-UpdateMinDelayWitness) +- [`UpdateGracePeriodWitness`](#timelock-UpdateGracePeriodWitness) +- [`SetOpenExecutorWitness`](#timelock-SetOpenExecutorWitness) + +Functions + +- [`new(min_delay_ms, grace_period_ms, ctx)`](#timelock-new) +- [`new_shared(min_delay_ms, grace_period_ms, ctx)`](#timelock-new_shared) +- [`share(self)`](#timelock-share) +- [`hash_operation(timelock_id, payload_digest, predecessor, salt)`](#timelock-hash_operation) +- [`schedule(self, _proposer_auth, params, predecessor, salt, delay_ms, clock, ctx)`](#timelock-schedule) +- [`execute(self, _executor_auth, id, clock, ctx)`](#timelock-execute) +- [`execute_open(self, id, clock, ctx)`](#timelock-execute_open) +- [`consume(self, ticket, _witness)`](#timelock-consume) +- [`cancel(self, _canceller_auth, id, ctx)`](#timelock-cancel) +- [`new_operation_cap(self)`](#timelock-new_operation_cap) +- [`operation_cap_timelock_id(cap)`](#timelock-operation_cap_timelock_id) +- [`destroy_operation_cap(cap)`](#timelock-destroy_operation_cap) +- [`schedule_with(self, cap, proposer_auth, params, predecessor, salt, delay_ms, clock, ctx)`](#timelock-schedule_with) +- [`execute_with(self, cap, executor_auth, id, clock, ctx)`](#timelock-execute_with) +- [`execute_open_with(self, cap, id, clock, ctx)`](#timelock-execute_open_with) +- [`cancel_with(self, cap, canceller_auth, id, ctx)`](#timelock-cancel_with) +- [`schedule_update_min_delay(self, _admin_auth, new_min_delay_ms, predecessor, salt, delay_ms, clock, ctx)`](#timelock-schedule_update_min_delay) +- [`execute_update_min_delay(self, _admin_auth, id, clock, ctx)`](#timelock-execute_update_min_delay) +- [`schedule_update_grace_period(self, _admin_auth, new_grace_period_ms, predecessor, salt, delay_ms, clock, ctx)`](#timelock-schedule_update_grace_period) +- [`execute_update_grace_period(self, _admin_auth, id, clock, ctx)`](#timelock-execute_update_grace_period) +- [`schedule_set_open_executor(self, _admin_auth, value, predecessor, salt, delay_ms, clock, ctx)`](#timelock-schedule_set_open_executor) +- [`execute_set_open_executor(self, _admin_auth, id, clock, ctx)`](#timelock-execute_set_open_executor) +- [`min_delay_ms(self)`](#timelock-min_delay_ms) +- [`grace_period_ms(self)`](#timelock-grace_period_ms) +- [`is_open_executor(self)`](#timelock-is_open_executor) +- [`max_delay_ms()`](#timelock-max_delay_ms) +- [`domain_tag()`](#timelock-domain_tag) +- [`proposer_role(self)`](#timelock-proposer_role) +- [`executor_role(self)`](#timelock-executor_role) +- [`canceller_role(self)`](#timelock-canceller_role) +- [`admin_role(self)`](#timelock-admin_role) +- [`is_operation(self, id)`](#timelock-is_operation) +- [`is_operation_pending(self, id, clock)`](#timelock-is_operation_pending) +- [`is_operation_ready(self, id, clock)`](#timelock-is_operation_ready) +- [`is_operation_expired(self, id, clock)`](#timelock-is_operation_expired) +- [`is_operation_done(self, id)`](#timelock-is_operation_done) +- [`operation_state(self, id, clock)`](#timelock-operation_state) +- [`operation_params(self, id)`](#timelock-operation_params) + +Events + +- [`TimelockCreated`](#timelock-TimelockCreated) +- [`OperationScheduled`](#timelock-OperationScheduled) +- [`OperationExecuted`](#timelock-OperationExecuted) +- [`OperationCancelled`](#timelock-OperationCancelled) +- [`MinDelayChanged`](#timelock-MinDelayChanged) +- [`GracePeriodChanged`](#timelock-GracePeriodChanged) +- [`OpenExecutorChanged`](#timelock-OpenExecutorChanged) + +Errors + +- [`EWrongRole`](#timelock-EWrongRole) +- [`EInvalidConfig`](#timelock-EInvalidConfig) +- [`EDelayTooShort`](#timelock-EDelayTooShort) +- [`EOperationAlreadyExists`](#timelock-EOperationAlreadyExists) +- [`EOperationUnset`](#timelock-EOperationUnset) +- [`EOperationAlreadyDone`](#timelock-EOperationAlreadyDone) +- [`EDelayNotElapsed`](#timelock-EDelayNotElapsed) +- [`EOperationExpired`](#timelock-EOperationExpired) +- [`EPredecessorNotDone`](#timelock-EPredecessorNotDone) +- [`EPredecessorUnset`](#timelock-EPredecessorUnset) +- [`EPredecessorIsSelf`](#timelock-EPredecessorIsSelf) +- [`EOpenExecutorDisabled`](#timelock-EOpenExecutorDisabled) +- [`EWrongTimelock`](#timelock-EWrongTimelock) +- [`EScheduleOverflow`](#timelock-EScheduleOverflow) +- [`EWrongParams`](#timelock-EWrongParams) +- [`EWrongAction`](#timelock-EWrongAction) +- [`EInvalidPredecessor`](#timelock-EInvalidPredecessor) + +#### Types [!toc] [#timelock-Types] + + +The per-protocol timelock registry. Shared object. + +`key`-only (no `store`): the only legal disposition is sharing, so a `Timelock` is always a shared object and can never be wrapped or address-owned. It stores the configuration (`min_delay_ms`, `grace_period_ms`, `open_executor`), the four bound role types, and a table mapping operation ids (keccak256, 32 bytes) to per-op state; absence of an id means `Unset`. Typed operation params are stored as dynamic fields directly on its `id`, keyed by the operation id. + + + +Stored per-operation state: `Pending { ready_at_ms, expires_at_ms, predecessor, action }` or `Done`. + +Timestamps and predecessor are locked at schedule time, so later `min_delay_ms` / `grace_period_ms` changes never move an existing op. `action` records the scheduled `Action` type so `execute` can re-check it (the id commits the `Action`, but execute takes the id directly). + + + +Deferred authorization for one typed operation, carrying the scheduled params. + +Hot potato with NO abilities, so it must be consumed in the same transaction it is minted. Minted by `execute` / `execute_open` (or in-module for self-admin ops); destroyed only by `consume` (or in-module for self-admin ops). + + + +A stored handle that binds an operation kind `(Action, Params)` to ONE `Timelock` instance. Carries NO authority on its own; combine it with `&Auth` for role authorization. + +Mint one per operation kind at consumer `init` via `new_operation_cap` (against your canonical timelock) and store it inside the object you protect; the `*_with` entries then enforce the canonical-timelock binding for you, so you never hand-write the `object::id` assert. `store`-only: it cannot be copied or dropped, so it lives in your protected object. + + + +BCS preimage of every operation id, with fields `action`, `payload_digest`, `predecessor`, `salt`, `timelock_id` (in declaration order). + +Including `timelock_id` makes identical inputs hash to different ids across `Timelock` instances. + + + +Observable operation state, computed from the stored `OpTimestamp` plus the `Clock`: `Unset`, `Waiting { ready_at_ms, expires_at_ms }`, `Ready { ready_at_ms, expires_at_ms }`, `Expired { ready_at_ms, expires_at_ms }`, or `Done`. + + + +Module-private marker witness (the `Action`) for the self-administered `min_delay_ms` pipeline. Only this module can construct it, so only its own execute path can mint/redeem the corresponding `ExecutionTicket`. + + + +Module-private marker witness (the `Action`) for the self-administered `grace_period_ms` pipeline. Only this module can construct it, so only its own execute path can mint/redeem the corresponding `ExecutionTicket`. + + + +Module-private marker witness (the `Action`) for the self-administered `open_executor` pipeline. Only this module can construct it, so only its own execute path can mint/redeem the corresponding `ExecutionTicket`. + + +#### Functions [!toc] [#timelock-Functions] + + +Mints a fresh `Timelock` bound to the four consumer role types. `min_delay_ms` is the floor on every operation's delay (may be 0); `grace_period_ms` is the window, after an op becomes ready, during which it stays executable. + +Returns the unshared `Timelock`; the caller must dispose of it via `share` (or use `new_shared`). + +Aborts with `EInvalidConfig` if `min_delay_ms > MAX_DELAY_MS`, or if `grace_period_ms` is zero or greater than `MAX_DELAY_MS`. + +Emits a `TimelockCreated` event. + + + +Convenience constructor that shares the `Timelock` and returns its `ID`. + +Aborts with `EInvalidConfig` on the same config bounds as `new`. + +Emits a `TimelockCreated` event. + + + +Shares a `Timelock`. The only legal way to dispose of a value returned by `new`. + + + +Pure operation-id derivation from a payload digest. Off-chain tooling computes `payload_digest = keccak256(bcs(params))` and reproduces the id from there. Returns the 32-byte operation id. + +The id is `keccak256(DOMAIN_TAG || bcs(IdInput))`, byte-exact: + +- `DOMAIN_TAG` is the ASCII string `OZ_Timelock_1_Sui` (readable via `domain_tag`). It is not length-prefixed: the preimage starts with its raw 17 bytes. +- `IdInput` fields are BCS-encoded in declaration order: `action`, `payload_digest`, `predecessor`, `salt`, `timelock_id`. +- `action` is `type_name::with_original_ids()`: the fully-qualified type string (`::::`, 64 lowercase hex chars, no `0x` prefix, type arguments included), resolved with the ORIGINAL package address so ids stay stable across package upgrades. Its BCS is a ULEB128 length prefix followed by those ASCII bytes. +- `payload_digest`, `predecessor`, and `salt` are ULEB128-length-prefixed byte vectors (an empty `predecessor` encodes as the single byte `0x00`); `timelock_id` is the raw 32 address bytes, unprefixed. + +Cross-check any off-chain implementation against this function before relying on predicted ids. + + + +Schedules an operation, storing its typed `params` on-chain. Caller must hold the proposer role. `predecessor` is the id of an op that must be `Done` before this one (or empty for none); `salt` disambiguates otherwise-identical operations; `delay_ms` must be at least `min_delay_ms`. Returns the operation id (pass to `execute` / `cancel`). This raw entry does not bind the `Timelock` id. Prefer `schedule_with`. + +Aborts with `EWrongRole` if `Role` is not the bound `proposer_role`. + +Aborts with `EDelayTooShort` if `delay_ms < min_delay_ms`. + +Aborts with `EScheduleOverflow` if `now + delay_ms` (or that sum plus `grace_period_ms`) overflows u64. + +Aborts with `EInvalidPredecessor` if `predecessor` is non-empty and not a 32-byte id. + +Aborts with `EPredecessorIsSelf` if `predecessor` equals the computed id (defense in depth; unreachable in practice because the id hashes over `predecessor`). + +Aborts with `EOperationAlreadyExists` if the id is already scheduled. + +Emits an `OperationScheduled` event. + + + +Executes a ready operation by id. Caller must hold the executor role. Marks the op `Done` (sticky), removes the stored params, and returns an `ExecutionTicket` carrying them: a no-ability hot potato that must be consumed by `consume` in the same PTB. This raw entry does not bind the `Timelock` id. Prefer `execute_with`. + +Aborts with `EWrongRole` if `Role` is not the bound `executor_role`. + +Aborts with `EOperationUnset` if no operation with this id exists. + +Aborts with `EOperationAlreadyDone` if the operation has already been executed. + +Aborts with `EDelayNotElapsed` if the operation's delay has not elapsed yet. + +Aborts with `EOperationExpired` if the operation's grace window has closed. + +Aborts with `EPredecessorUnset` if the operation names a predecessor that is not in the timelock. + +Aborts with `EPredecessorNotDone` if the operation names a predecessor that is not yet executed. + +Aborts with `EWrongAction` if `Action` does not match the type the operation was scheduled with. + +Aborts with `EWrongParams` if `Params` does not match the type the operation was scheduled with. + +Emits an `OperationExecuted` event. + + + +Executes a ready operation in open-executor mode (no `Auth` required). Open mode lifts only the executor-role gate on minting the ticket; consumption stays witness-gated, so a caller who cannot construct `Action` cannot `consume` the returned ticket. Since it has no abilities, their transaction aborts and reverts the state change atomically. + +Aborts with `EOpenExecutorDisabled` if `open_executor` is `false`. + +Aborts on the same operation-state conditions as `execute`. + +Emits an `OperationExecuted` event. + + + +Redeems an execution ticket. Two gates fire: timelock-binding and witness-by-value (`Action` is consumed, so only a module that can construct `Action` can call this). Returns `(op_id, params)`, the operation id and the exact typed params committed at schedule time; there is no payload to re-supply or mismatch. + +Aborts with `EWrongTimelock` if the ticket was minted by a different `Timelock`. + + + +Cancels a scheduled operation by id, dropping its stored params. Caller must hold the canceller role. Allowed on `Waiting` / `Ready` / `Expired` operations; not on `Done`. The operation's `Params` type must be named so the stored params can be cleaned up. This raw entry does not bind the `Timelock` id. Prefer `cancel_with`. + +Aborts with `EWrongRole` if `Role` is not the bound `canceller_role`. + +Aborts with `EOperationUnset` if no such operation exists. + +Aborts with `EOperationAlreadyDone` if the operation was already executed. + +Aborts with `EWrongParams` if `Params` does not match the type the operation was scheduled with. + +Emits an `OperationCancelled` event. + + + +Mints an `OperationCap` binding `(Action, Params)` to this `Timelock`. Permissionless and authority-free: call it once at consumer `init` against your canonical timelock and store the result in the object you protect. + + + +Returns the `Timelock` id an `OperationCap` is bound to. + + + +Destroys an `OperationCap`, e.g. when decommissioning the object that stored it. An `OperationCap` has `store` but not `drop`, so it cannot be discarded implicitly; this is the explicit disposal. The cap carries no authority; a fresh one is always mintable via `new_operation_cap`. + + + +Like `schedule`, but the `OperationCap` enforces the canonical-timelock binding, and `Action` / `Params` infer from the cap (zero explicit type args at the call site). Recommended over the raw entry. Returns the operation id. + +Aborts with `EWrongTimelock` if `cap` is not bound to `self`; plus the same aborts as `schedule`. + +Emits an `OperationScheduled` event. + + + +Like `execute`, but the `OperationCap` enforces the canonical-timelock binding. Recommended over the raw entry. Returns an `ExecutionTicket` that must be consumed in the same PTB. + +Aborts with `EWrongTimelock` if `cap` is not bound to `self`; plus the same aborts as `execute`. + +Emits an `OperationExecuted` event. + + + +Like `execute_open`, but the `OperationCap` enforces the canonical-timelock binding. Returns an `ExecutionTicket` that must be consumed in the same PTB. + +Aborts with `EWrongTimelock` if `cap` is not bound to `self`; plus the same aborts as `execute_open`. + +Emits an `OperationExecuted` event. + + + +Like `cancel`, but the `OperationCap` enforces the canonical-timelock binding. Recommended over the raw entry. + +Aborts with `EWrongTimelock` if `cap` is not bound to `self`; plus the same aborts as `cancel`. + +Emits an `OperationCancelled` event. + + + +Schedules a `min_delay_ms` change (stored as the operation's params). Admin-gated. Returns the operation id (pass to `execute_update_min_delay`). + +Aborts with `EWrongRole` if `Role` is not the bound `admin_role`. + +Aborts with `EInvalidConfig` if `new_min_delay_ms > MAX_DELAY_MS`. + +Aborts on the scheduling conditions of `schedule` (`EDelayTooShort`, `EScheduleOverflow`, `EInvalidPredecessor`, `EPredecessorIsSelf` (unreachable in practice), `EOperationAlreadyExists`). + +Emits an `OperationScheduled` event. + + + +Executes a scheduled `min_delay_ms` change by id. Admin-gated. Applies the stored value; the ticket is minted and consumed in-module (the marker witness is only constructible here). + +Aborts with `EWrongRole` if `Role` is not the bound `admin_role`. + +Aborts on the same conditions as `execute`, apart from its role check. + +Aborts with `EInvalidConfig` if the stored `new_min_delay_ms` exceeds `MAX_DELAY_MS`. The bound is re-asserted at apply time because the op can be staged through the generic `schedule`. + +Emits an `OperationExecuted` event, and a `MinDelayChanged` event when the configured value actually changes (a no-op update emits no `MinDelayChanged`). + + + +Schedules a `grace_period_ms` change. Admin-gated. Returns the operation id (pass to `execute_update_grace_period`). + +Aborts with `EWrongRole` if `Role` is not the bound `admin_role`. + +Aborts with `EInvalidConfig` if `new_grace_period_ms` is zero or greater than `MAX_DELAY_MS`. + +Aborts on the scheduling conditions of `schedule` (`EDelayTooShort`, `EScheduleOverflow`, `EInvalidPredecessor`, `EPredecessorIsSelf` (unreachable in practice), `EOperationAlreadyExists`). + +Emits an `OperationScheduled` event. + + + +Executes a scheduled `grace_period_ms` change by id. Admin-gated. Applies the stored value. + +Aborts with `EWrongRole` if `Role` is not the bound `admin_role`. + +Aborts on the same conditions as `execute`, apart from its role check. + +Aborts with `EInvalidConfig` if the stored `new_grace_period_ms` is zero or exceeds `MAX_DELAY_MS`. The bound is re-asserted at apply time because the op can be staged through the generic `schedule` (a zero grace period would brick the timelock with an empty ready window). + +Emits an `OperationExecuted` event, and a `GracePeriodChanged` event when the configured value actually changes (a no-op update emits no `GracePeriodChanged`). + + + +Schedules an `open_executor` toggle. Admin-gated. `value` is the setting applied when the scheduled op executes. Returns the operation id (pass to `execute_set_open_executor`). + +Aborts with `EWrongRole` if `Role` is not the bound `admin_role`. + +Aborts on the scheduling conditions of `schedule` (`EDelayTooShort`, `EScheduleOverflow`, `EInvalidPredecessor`, `EPredecessorIsSelf` (unreachable in practice), `EOperationAlreadyExists`). + +Emits an `OperationScheduled` event. + + + +Executes a scheduled `open_executor` toggle by id. Admin-gated. + +Aborts with `EWrongRole` if `Role` is not the bound `admin_role`. + +Aborts on the same conditions as `execute`, apart from its role check. + +Emits an `OperationExecuted` event, and an `OpenExecutorChanged` event when the setting actually toggles (re-applying the current setting emits no `OpenExecutorChanged`). + + + +Returns the configured floor on every operation's delay. + + + +Returns the configured window, after an op becomes ready, during which it stays executable. + + + +Returns whether open-executor mode is enabled (anyone may call `execute_open`). + + + +Returns the upper bound on the configured `min_delay_ms` and `grace_period_ms` (60 days in milliseconds). Does NOT bound the per-call `delay_ms`. + + + +Returns the domain separation tag prefixed to every operation-id preimage: the ASCII string `OZ_Timelock_1_Sui`. `OZ_Timelock` names the primitive, `1` versions the preimage format, `Sui` pins the chain. Locked at publication. + + + +Returns the role type bound for scheduling. + + + +Returns the role type bound for executing. + + + +Returns the role type bound for cancelling. + + + +Returns the role type bound for the self-administered configuration pipeline. + + + +Returns `true` if an operation with this id exists in the timelock (any state but `Unset`: `Waiting`, `Ready`, `Expired`, or `Done`); `false` otherwise. + + + +Returns `true` if the operation is on the execution track: `Waiting` or `Ready`. `Expired`, `Done`, and `Unset` operations return `false`. + +Narrower than [`isOperationPending`](https://docs.openzeppelin.com/contracts/5.x/api/governance#TimelockController-isOperationPending-bytes32-) in OpenZeppelin's Solidity `TimelockController`, where operations never expire and pending doubles as the cancellability check. Here an `Expired` operation is no longer pending but is still cancellable. To gate cancellation or cleanup, use `is_operation(id) && !is_operation_done(id)` (or match on `operation_state`). + + + +Returns `true` if the operation is `Ready`: its delay has elapsed and its grace window is still open, so it can be executed now (predecessor permitting); `false` otherwise. + + + +Returns `true` if the operation is `Expired`: its grace window has closed, so it can no longer be executed and can only be cancelled; `false` otherwise. + + + +Returns `true` if the operation has been executed (`Done`); `false` otherwise. + + + +Returns the observable `OperationState` of an operation at the current clock time: `Unset`, `Waiting`, `Ready`, `Expired`, or `Done`. + + + +Borrows the typed params of a scheduled, not-yet-executed operation (`Waiting`, `Ready`, or `Expired`) for off-chain inspection and UIs. + +Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the id has no stored params (`Unset` or already `Done`). + +Aborts with `sui::dynamic_field::EFieldTypeMismatch` if `Params` does not match the type the operation was scheduled with. + + +#### Events [!toc] [#timelock-Events] + + +Emitted by `new` when a `Timelock` is created, recording its initial config and the four bound role types. + + + +Emitted when an operation is committed by `schedule` or by the self-administered `schedule_update_min_delay` / `schedule_update_grace_period` / `schedule_set_open_executor` (whose `proposer` is an admin-role holder). `payload_digest` is `keccak256(bcs(params))`; the params themselves are not emitted. + + + +Emitted when an operation is executed and its ticket minted by `execute` / `execute_open` or by the self-administered `execute_update_min_delay` / `execute_update_grace_period` / `execute_set_open_executor`, which emit it alongside the corresponding `*Changed` event in the same call. + + + +Emitted by `cancel` / `cancel_with` when a pending (`Waiting` / `Ready` / `Expired`) operation is cancelled. `action` is the scheduled `Action` type of the cancelled operation. + + + +Emitted by `execute_update_min_delay` when the configured `min_delay_ms` actually changes; a no-op update (applying the already-configured value) emits no event. + + + +Emitted by `execute_update_grace_period` when the configured `grace_period_ms` actually changes; a no-op update (applying the already-configured value) emits no event. + + + +Emitted by `execute_set_open_executor` when open-executor mode actually toggles; re-applying the current setting emits no event. + + +#### Errors [!toc] [#timelock-Errors] + + +Raised when the `&Auth` role type does not match the role bound for this action. + + + +Raised when `min_delay_ms` or `grace_period_ms` is outside the permitted bounds. + + + +Raised when `schedule` is called with `delay_ms` below the configured `min_delay_ms`. + + + +Raised when an operation with this id is already scheduled. + + + +Raised when no operation with this id exists in the timelock. + + + +Raised when the operation has already been executed. + + + +Raised when the operation's delay has not elapsed yet. + + + +Raised when the operation's grace window has closed; it can no longer be executed. + + + +Raised when the named predecessor is scheduled but not yet executed. + + + +Raised when the named predecessor is not present in the timelock. + + + +Raised when an operation names itself as its predecessor. Defense in depth: the id is the keccak256 of a preimage that includes `predecessor`, so a match would require a hash fixed point and is unreachable in practice. + + + +Raised when `execute_open` is called while open-executor mode is disabled. + + + +Raised when the execution ticket (or the `OperationCap` on the `*_with` path) does not belong to this timelock. + + + +Raised when scheduling this operation would overflow the u64 deadline arithmetic (`now + delay_ms`, or that sum plus `grace_period_ms`). + + + +Raised when the supplied `Params` type does not match the type the operation was scheduled with. Reachable on either path: the raw `&Auth` path takes `Params` explicitly, and the `*_with` (cap) path pins it via the `OperationCap`. Supplying a cap for a different `(Action, Params)` than the op was scheduled with still mismatches. + + + +Raised when the supplied `Action` type does not match the type the operation was scheduled with. The `Action` is hashed into the operation id, but `execute` takes the id directly, so the binding is re-checked at execute time against the stored `Action`. + + + +Raised when `predecessor` is non-empty but not a 32-byte operation id, so it could never match a scheduled op. Rejected at schedule time rather than leaving a silently un-executable op. + diff --git a/content/contracts-sui/1.x/index.mdx b/content/contracts-sui/1.x/index.mdx index 368b80bc..c3c9f017 100644 --- a/content/contracts-sui/1.x/index.mdx +++ b/content/contracts-sui/1.x/index.mdx @@ -2,11 +2,12 @@ title: Contracts for Sui 1.x --- -**OpenZeppelin Contracts for Sui v1.x** ships four core packages: +**OpenZeppelin Contracts for Sui v1.x** ships five core packages: - `openzeppelin_math` for deterministic integer arithmetic, configurable rounding, and decimal scaling. - `openzeppelin_fp_math` for 9-decimal fixed-point arithmetic (`UD30x9`, `SD29x9`) backed by `u128`. - `openzeppelin_access` for role-based authorization and ownership-transfer wrappers around privileged `key + store` objects. +- `openzeppelin_timelock` for enforcing a minimum on-chain delay between scheduling and executing privileged operations. - `openzeppelin_utils` for embeddable building blocks; its first module, `rate_limiter`, provides multi-strategy rate limiting. ## Quickstart @@ -37,7 +38,7 @@ You only need the dependencies your app actually uses. Add what you need and dro ### 3. Verify `Move.toml` -`mvr add` updates `Move.toml` automatically. With all three installed it should include: +`mvr add` updates `Move.toml` automatically. With all four installed it should include: ```toml [dependencies] @@ -82,14 +83,15 @@ sui move test - Need role-based authorization for privileged functions or controlled transfer of admin/treasury/upgrade capabilities? Use [Access](/contracts-sui/1.x/access). - Need fractional values like prices, fees, rates, or signed deltas? Use [Fixed-Point Math](/contracts-sui/1.x/fixed-point). - Need integer arithmetic with safe overflow and explicit rounding? Use [Integer Math](/contracts-sui/1.x/math). +- Need to enforce a minimum delay before privileged operations execute? Use [Timelock](/contracts-sui/1.x/timelock). - Need to throttle withdrawals, meter per-user budgets, gate action reuse, or delay an action? Use [Rate Limiter](/contracts-sui/1.x/rate-limiter). The packages compose. A typical protocol module imports `openzeppelin_math` for share math, `openzeppelin_fp_math` for rate and fee math, and `openzeppelin_access` for the admin capability that governs both. ## Next steps -- Package guides: [Integer Math](/contracts-sui/1.x/math), [Fixed-Point Math](/contracts-sui/1.x/fixed-point), [Access](/contracts-sui/1.x/access), [Utilities](/contracts-sui/1.x/utils). +- Package guides: [Integer Math](/contracts-sui/1.x/math), [Fixed-Point Math](/contracts-sui/1.x/fixed-point), [Access](/contracts-sui/1.x/access), [Timelock](/contracts-sui/1.x/timelock), [Utilities](/contracts-sui/1.x/utils). - Access modules: [RBAC](/contracts-sui/1.x/access-control), [Two-Step Transfer](/contracts-sui/1.x/two-step-transfer), [Delayed Transfer](/contracts-sui/1.x/delayed-transfer). - Utilities modules: [Rate Limiter](/contracts-sui/1.x/rate-limiter). - Learn: [Role Based Access Control](/contracts-sui/1.x/guides/access-control). -- API reference: [Integer Math](/contracts-sui/1.x/api/math), [Fixed-Point Math](/contracts-sui/1.x/api/fixed-point), [Access](/contracts-sui/1.x/api/access), [Utilities](/contracts-sui/1.x/api/utils). +- API reference: [Integer Math](/contracts-sui/1.x/api/math), [Fixed-Point Math](/contracts-sui/1.x/api/fixed-point), [Access](/contracts-sui/1.x/api/access), [Timelock](/contracts-sui/1.x/api/timelock), [Utilities](/contracts-sui/1.x/api/utils). diff --git a/content/contracts-sui/1.x/timelock.mdx b/content/contracts-sui/1.x/timelock.mdx new file mode 100644 index 00000000..8eb54b66 --- /dev/null +++ b/content/contracts-sui/1.x/timelock.mdx @@ -0,0 +1,120 @@ +--- +title: Timelock +--- + +The `openzeppelin_timelock` package is a Sui-native take on OpenZeppelin's `TimelockController`: it enforces a minimum on-chain delay between scheduling a privileged operation and executing it, giving users a window to react before the change takes effect. + +Because Move has no `target.call(data)`, the timelock stores each operation's typed parameters on-chain and, at execution time, hands them back through a no-ability `ExecutionTicket` hot potato that the consumer redeems in the same transaction. The consumer dispatches to itself. An integration binds to a specific `Timelock` through a stored `OperationCap`, so the canonical-timelock check is enforced by the library rather than by hand-written asserts. Roles (proposer, executor, canceller, admin) come from a hard dependency on `openzeppelin_access::access_control`. + + +Prefer the `OperationCap`-bound entries (`schedule_with`, `execute_with`, `cancel_with`). The raw `schedule` / `execute` / `cancel` entries do not bind the timelock id: if you use them you must assert `object::id(timelock)` against your canonical timelock in every wrapper, and forgetting it silently disables the delay. Also, `ExecutionTicket` has no abilities. Always follow `execute` with `consume` in the same PTB, or the transaction aborts. + + +## Usage + +The package is not yet published to the Move Registry (MVR), so install it as a git dependency pinned to the release tag in `Move.toml`: + +```toml +[dependencies] +openzeppelin_timelock = { git = "https://github.com/OpenZeppelin/contracts-sui.git", subdir = "contracts/timelock", rev = "v1.5.0" } +openzeppelin_access = { git = "https://github.com/OpenZeppelin/contracts-sui.git", subdir = "contracts/access", rev = "v1.5.0" } +``` + +The `openzeppelin_access` dependency is needed whenever your module names its types directly (e.g. `Auth`), as the example below does. + +Import the module: + +```move +use openzeppelin_timelock::timelock::{Self, Timelock, OperationCap}; +``` + +## Examples + +A single `Timelock` gating an AMM fee change. The `OperationCap` lives on the `Pool`, so the canonical-timelock binding is structural and call sites carry zero explicit type arguments: + +```move +module my_protocol::amm; + +use openzeppelin_access::access_control::{Self, Auth}; +use openzeppelin_timelock::timelock::{Self, Timelock, OperationCap}; +use sui::clock::Clock; + +public struct AMM has drop {} + +public struct ProposerRole {} +public struct ExecutorRole {} +public struct CancellerRole {} +public struct TimelockAdminRole {} + +// Drop-only witness: construction never leaves this module. +public struct FeeChangeAction has drop {} + +public struct Pool has key { + id: UID, + fee_bps: u16, + // Binds fee-change operations to the canonical timelock. + fee_cap: OperationCap, +} + +fun init(otw: AMM, ctx: &mut TxContext) { + // Roles default to root-role (`AMM`) administration: the default admin + // grants ProposerRole / ExecutorRole / CancellerRole memberships + // post-publish via `grant_role`. + let ac = access_control::new(otw, 7 * 86_400_000, ctx); + + // `new` + `share` so the cap can be minted before the timelock is shared. + let tl = timelock::new( + 86_400_000, // 24h minimum delay + 604_800_000, // 7d grace period + ctx, + ); + let fee_cap = tl.new_operation_cap(); + tl.share(); + transfer::share_object(Pool { id: object::new(ctx), fee_bps: 30, fee_cap }); + transfer::public_share_object(ac); +} + +// A proposer schedules; the OperationCap binds the call to the canonical timelock. +public fun schedule_fee_change( + tl: &mut Timelock, + pool: &Pool, + proposer: &Auth, + new_fee_bps: u16, + salt: vector, + delay_ms: u64, + clock: &Clock, + ctx: &mut TxContext, +): vector { + // Empty predecessor: this operation depends on no other. + tl.schedule_with(&pool.fee_cap, proposer, new_fee_bps, vector[], salt, delay_ms, clock, ctx) +} + +// An executor cranks the ready operation; `consume` hands back the typed params. +public fun execute_fee_change( + tl: &mut Timelock, + pool: &mut Pool, + executor: &Auth, + id: vector, + clock: &Clock, + ctx: &mut TxContext, +) { + let ticket = tl.execute_with(&pool.fee_cap, executor, id, clock, ctx); + // The ticket MUST be consumed in the same PTB. + let (_op_id, new_fee_bps) = tl.consume(ticket, FeeChangeAction {}); + pool.fee_bps = new_fee_bps; +} +``` + +More complete integrations (an AMM with self-administered config, dual governance with two timelocks, and a timelocked package `UpgradeCap`) live in [`examples/timelock`](https://github.com/OpenZeppelin/contracts-sui/tree/v1.5.0/contracts/timelock/examples/timelock) on GitHub. + +## Key concepts + +- `Timelock` (shared object): the per-protocol registry holding each operation's state machine and typed params. Operation ids are deterministic keccak256 hashes, reproducible off-chain and isolated per instance. +- `OperationCap` (stored): binds one operation kind to one `Timelock`. Minted at `init` and stored in the object you protect; carries no authority on its own. +- `ExecutionTicket` (hot potato): minted by `execute` / `execute_with`, carries the typed params, and must be redeemed with `consume` (witness-gated) in the same PTB. +- Roles: every gated entry takes `&Auth` from `openzeppelin_access` and checks it against the proposer / executor / canceller / admin role type bound at creation. Optional open-executor mode makes execution permissionless. +- Delay and expiry: an operation is executable in the half-open window `[ready_at_ms, expires_at_ms)`, locked at schedule time. Later config changes never move an in-flight op, and `Done` is sticky, so operations are non-replayable. `min_delay_ms`, `grace_period_ms`, and `open_executor` are self-administered: every change is itself timelocked. + +## API Reference + +Use the full function-level reference here: [Timelock API](/contracts-sui/1.x/api/timelock). diff --git a/content/contracts-sui/index.mdx b/content/contracts-sui/index.mdx index ba9f99a1..d91a0940 100644 --- a/content/contracts-sui/index.mdx +++ b/content/contracts-sui/index.mdx @@ -29,6 +29,9 @@ import { latestStable } from "./latest-versions.js"; Overflow-safe integer arithmetic with explicit rounding and decimal scaling helpers. + + Package-level guide for the delayed-operation controller, including scheduling model, operation caps, and execution tickets. + Embeddable rate limiters for throttling on-chain actions. @@ -46,6 +49,9 @@ import { latestStable } from "./latest-versions.js"; Explore the complete integer math API, including its functions, types, and errors. + + Explore the complete timelock API, including all functions, types, emitted events, and expected error conditions for integration. + Explore the complete utilities API, including the rate limiter types, functions, and errors. diff --git a/public/llms.txt b/public/llms.txt index c706723f..1f5c4f1f 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -316,11 +316,13 @@ Each ecosystem section lists the smart-contract libraries and language-specific - [Packages — Access — RBAC](https://docs.openzeppelin.com/contracts-sui/1.x/access-control) - [Packages — Access — Two-Step Transfer](https://docs.openzeppelin.com/contracts-sui/1.x/two-step-transfer) - [Packages — Access — Delayed Transfer](https://docs.openzeppelin.com/contracts-sui/1.x/delayed-transfer) +- [Packages — Timelock](https://docs.openzeppelin.com/contracts-sui/1.x/timelock) - [Packages — Utils — Overview](https://docs.openzeppelin.com/contracts-sui/1.x/utils) - [Packages — Utils — Rate Limiter](https://docs.openzeppelin.com/contracts-sui/1.x/rate-limiter) - [API Reference — Integer Math](https://docs.openzeppelin.com/contracts-sui/1.x/api/math) - [API Reference — Fixed-Point Math](https://docs.openzeppelin.com/contracts-sui/1.x/api/fixed-point) - [API Reference — Access](https://docs.openzeppelin.com/contracts-sui/1.x/api/access) +- [API Reference — Timelock](https://docs.openzeppelin.com/contracts-sui/1.x/api/timelock) - [API Reference — Utils](https://docs.openzeppelin.com/contracts-sui/1.x/api/utils) ## Midnight diff --git a/src/navigation/sui/current.json b/src/navigation/sui/current.json index 55aef69f..7a7981c2 100644 --- a/src/navigation/sui/current.json +++ b/src/navigation/sui/current.json @@ -65,6 +65,11 @@ } ] }, + { + "type": "page", + "name": "Timelock", + "url": "/contracts-sui/1.x/timelock" + }, { "type": "folder", "name": "Utils", @@ -103,6 +108,11 @@ "name": "Access", "url": "/contracts-sui/1.x/api/access" }, + { + "type": "page", + "name": "Timelock", + "url": "/contracts-sui/1.x/api/timelock" + }, { "type": "page", "name": "Utils",