Repository navigation
feat(timelock): reconstruct the delayed-execution admin crate - #294
Merged
techisigu merged 1 commit intoOct 24, 2026
Merged
Conversation
AminMG044
force-pushed
the
feat/timelock-reconstruction
branch
from
October 19, 2026 17:12
284b99c to
82f0033
Compare
AminMG044
force-pushed
the
feat/timelock-reconstruction
branch
2 times, most recently
from
October 21, 2026 21:27
408294a to
3e8e24e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
timelock/was empty onmainapart from a migrations shim, even thoughREADME.mddocuments it in full: a delayed-execution admin withproposer / executor / canceller roles and predecessor dependencies,
meant to hold the
adminrole on the other contracts so users always get anexit window before a destructive action lands.
This rebuilds the crate to match that documented design — the same function
signatures, the same view names, the same event names, the same error codes —
and wires the existing migrations shim back in.
Related Issue
Closes #166
Changes
[ADD]
timelock/src/lib.rs— the contractinitialize(min_delay, proposers, executors, cancellers)— one-timesetup. There is no admin: the timelock's own configuration changes only
through a delayed operation. An empty
executorslist means anyone mayexecute a ready operation (an "open executor", as in OpenZeppelin's
TimelockController).
schedule(proposer, calls, predecessor, salt, delay) -> id— proposer-only.callsis aVec<Call { target, function, args }>,delay >= min_delay,and the id is
sha256(xdr((calls, predecessor, salt)))— so the id commitsto the whole payload and
hash_operationderives it off-chain, beforeanything is scheduled.
execute(executor, calls, predecessor, salt)— executor-only (unless open).The operation is identified by the content the executor repeats, which
hashes to the id that was scheduled. Every call runs in order via
invoke_contract; a revert in any of them reverts the whole batch.cancel(canceller, id)— canceller-only.Callwhose targetis the timelock itself is dispatched internally by
execute:update_delay(u64),grant_role(Role, Address),revoke_role(Role, Address),set_open_executor(bool)— andmigrate(u32, u32), which the migrations shim needs. These are thereforeonly reachable through a delayed operation.
get_timestamp(id)(0 unknown, 1 done, else ready-at),is_operation,is_operation_pending,is_operation_ready,is_operation_done,get_operation,get_min_delay,has_role(role, account),is_open_executor,hash_operation.[ADD]
timelock/src/types.rs—Call,Role,Operation,DataKeyEach role is a set of addresses, not a single holder: the protocol hands
the timelock to one signer during the handover and to a council later,
without the contract having to be redeployed.
[ADD]
timelock/src/error.rs— the documented error codes1
AlreadyInitialized, 2Unauthorized, 3InsufficientDelay,4
AlreadyScheduled, 5NotReady, 6PredecessorNotDone, 7NotPending,8
UnknownSelfCall, 9EmptyOperation— plus the migration codes 100–104that
zenith_common::migrationsexpects every contract to carry.[ADD]
timelock/src/events.rs—scheduled,executed,cancelled,min_delay_changed,role_changed,open_executor_changed.[MODIFY]
timelock/src/migrations.rs— the shim, preserved and workingThe step registry,
require_currentandself_migrateare untouched. Thecrate now declares
mod migrations;, carries the migration error variants,writes the schema version in
initialize, and dispatches themigrateself-call — the four things the shim's own note asked for. The stale note
("this crate's lib.rs … is empty on main") is updated to say it is restored.
[MODIFY]
timelock/Cargo.toml—zenith-commonis now a dependencyThe shim needs the shared migration framework.
[ADD]
Cargo.lock— the workspace had no lockfileWithout one,
cargoresolvesed25519-dalekto 3.0.0 forsoroban-env-host, whose>=2.0.0requirement admits it, and the buildfails with a
ChaCha20Rng: CryptoRngerror before a single test runs. Thelockfile pins
ed25519-dalekto the 2.x line the workspace already asks for.This is what makes the timelock's own CI jobs (
clippy,wasm,test)build at all.
[MODIFY]
README.md— the timelock referenceThe section that specifies this contract now also records the two things the
implementation had to pin down: the delay boundary is inclusive (an
operation may run at exactly its ready-at, and not before), and a cancelled
predecessor can never run, so neither can its dependent.
[MODIFY]
.github/workflows/ci.yml— the workflow file was invalid YAMLci.ymlonmaindoes not parse. A bad merge left a straycrate: [...]fragment at the top level, a truncated matrix list (
tools/resource-sna),and three jobs with the same key (
wasm), one of them with twosteps:keys. GitHub rejects the file, so no workflow run has been created since
2026-09-30 — every CI run in this repo ends with zero jobs, on
mainand onevery open PR.
This repairs the three breakages and nothing else:
wasm:jobs get their real names back —wasm(the matrix thatruns
cargo xtask test),build(cargo xtask build+cargo xtask test),and
wasm-release(the release build,check-wasmand the artifactupload).
Without this, the timelock's own CI jobs cannot run at all.
Verification Results
initializeestablishing the three roles (and compatibility withcommon's admin/multisig pattern where applicable)initialize(min_delay, proposers, executors, cancellers); the timelock is the admin of the other contracts and satisfies theiradmin.require_auth()by being the direct invoker — the sameAuth::Adminshapezenith-commonspeaksschedule/propose,execute, andcancelentrypoints with a configurable delay, role-gated per the README's tablemin_delayand a per-operationdelayPredecessorNotDone; a cancelled predecessor can never run, so its dependent can never run eitherscheduled/executed/cancelledevents; the id issha256(xdr((calls, predecessor, salt))), derivable off-chain withhash_operationand readable back throughget_timestamp/is_operation*require_currentandself_migrateuntouched;init_schemaininitialize; themigrateself-call dispatches into the framework (a test asserts an invalid target is refused)NotReadyNotPending(a done operation is not pending)NotPendingeta) is decided and testedeta)Type of change
*.corridor.yamlonly — no engine change)Checklist
cargo fmt --checkpasses for the timelock cratecargo clippy --all-targets -- -D warningspassescargo testpasses (23 tests)cargo build --target wasm32-unknown-unknown --releasesucceeds.envfiles committedNotes for the reviewer
adminonoptions_market/price_oracle/vault, and the governance handoverscripts in
scripts/governance/, which still speak the old timelockinterface (
schedule(op_id, targets, fns, args, delay),get_proposer,get_guardian). Those scripts were already broken — the crate was empty —and re-pointing them at this interface belongs with the integration issue.
fmt/clippy/wasmCI jobs are red onmainbefore this change, for reasons owned by other issues:
price_oracle,vaultandoptions_marketare still stubs (feat(vault): multi-token ledger, namespaced tags, tag ownership, balance-delta accounting #147, feat(vault,timelock): ledger index, batch settlement ops, timelock controller #148, feat: batch trading, paginated views, multisig executor and action registry #149) andstreams/staking/test_adversariesdo not compile. The timelock'sown gates are green — its
testmatrix entry (cargo fmt --checkintimelock/) passes, and its crate builds, tests and lints clean.Reference
The PR's structure — Overview / Related Issue / Changes / Verification Results
with an acceptance-criteria table — follows
Streampay-Org/StreamPay-Frontend#1282.