Timelock
CertichainTimelock puts a public delay in front of every admin action on the registries. After the handover, registering an institution, removing one, retiring a key, lifting an issuer suspension or changing a role must first be scheduled by the admin Safe, waits for the delay in plain view on the chain, and only then can be executed. Anyone watching (institutions, verifiers, the monitor) sees what is coming, and the Safe can cancel in between.
The contract
/// @notice The OpenZeppelin timelock with no changes. It is named here so that its artifact and ABI are built and exported
/// with the other v0.3 contracts. Deploy it with the Safe as the only proposer (the Safe is also made canceller), the zero
/// address as executor (anyone may run a ready operation) and no admin, so the timelock administers itself.
contract CertichainTimelock is TimelockController {
constructor(uint256 minDelay, address[] memory proposers, address[] memory executors, address admin)
TimelockController(minDelay, proposers, executors, admin)
{}
}
There is no Certichain logic in it. Its behaviour is OpenZeppelin 5's TimelockController; the project's tests and scripts cover only its configuration. It compiles with the exact pragma 0.8.28.
Intended configuration
| Setting | Value | Why |
|---|---|---|
| Proposer | The admin Safe (2 of 3 on Sepolia) only | Only the Safe can schedule |
| Canceller | The Safe (OpenZeppelin grants it with the proposer role) | The Safe can stop a scheduled action it regrets or that was forced |
| Executor | The zero address, meaning anyone | The action was approved when it was scheduled; running it needs no extra trust |
| Admin | None | The timelock administers itself: changing its delay or roles is itself a scheduled action |
| Delay | 1 hour on Sepolia, 2 days planned for production | A notice period long enough to react |
The deploy script (blockchain/scripts/lib/timelock.ts) refuses, before sending anything: a chain other than the manifest's; a Safe address that is invalid, zero, has no code or does not answer getThreshold(); a delay below 1 hour (MIN_DELAY_FLOOR_SEC) or above 30 days (MAX_DELAY_SEC); and a chain that already has a timelock recorded. After deployment it reads the roles back from the chain (verifyTimelockSetup) and records the address in the manifest only if they match the design. The deployer key holds no role on the timelock.
What it governs
The timelock becomes the default admin of the two registries that have one, in every set it is handed: IssuerRegistry and StatusRegistry. The other registries and the access gate have no admin, and the VersionAnnouncer is not moved by the scripts.
| Goes through the timelock | Stays instant |
|---|---|
addIssuer, removeIssuer, setIssuerName, retireIssuer | Guardian pause() |
setIssuerSuspended(issuer, false, reason) (lifting) | Guardian setIssuerSuspended(issuer, true, reason) (suspending) |
grantRole and revokeRole (for example the guardian role) | Guardian unpause() on sets from 0.5.0 (on 0.3.0 unpause is an admin action and waits) |
| Admin holds and reinstating them on the status registry | Every issuer action on its own credentials |
Changing the timelock's own delay (updateDelay), which waits for the old delay |
The handover from the Safe
The registries use OpenZeppelin's AccessControlDefaultAdminRules, so the admin role moves in two steps with a delay of its own (3 days by default, ADMIN_DELAY_SEC):
npm run timelock:handover:sepolia (in blockchain/) reads the chain and says which step is next: begin, wait, schedule, execute or done (planTimelockHandover in blockchain/scripts/lib/timelock-handover.ts). It writes Safe Transaction Builder files under blockchain/timelock-files/<chain>/ (handover-1-begin.json, handover-2-schedule.json, handover-3-execute.json) and uses a fixed salt, id("certichain-timelock-handover"). It first checks the recorded timelock and offers no step if its roles or delay differ from the design. Until the timelock accepts, the Safe can still call cancelDefaultAdminTransfer. After the accept, the Safe has no direct admin power.
A set deployed after the handover starts with its deployer as admin. npm run timelock:nominate:sepolia with SET=<label> makes the deployer nominate the timelock on that set's two registries, and the handover script, scoped with SET, plans the rest.
Everyday admin actions
blockchain/scripts/timelock-propose.ts takes TARGET (IssuerRegistry, StatusRegistry or Timelock), FUNCTION, ARGS (JSON), and optionally SET, SALT or SIGNOFF_FILE, DELAY_SEC and LABEL. It sends nothing. It prints the operation id, the salt and the earliest execution time, and writes <id>-1-schedule.json, <id>-2-execute.json and <id>-cancel.json. With SIGNOFF_FILE, the salt is the hash of an accreditor sign-off file, which ties an institution's admission to a recorded decision (npm run admission:check:sepolia compares them). See Governance.
Status on Sepolia (as of 2026-10-24)
| Item | State |
|---|---|
| Timelock | Deployed 2026-10-17 at 0x33a22Cc4D86A62Ae141349B768C054b1DD794f33, delay 3,600 seconds, the Safe as proposer and canceller, open executor, no admin; verified on Etherscan; recorded as timelock in src/contracts/v03/deployment.11155111.json |
| Set 0.5.0 (not active) | Both registries administered by the timelock. Issuers were added and one key retired through scheduled Safe batches during the rehearsal |
| Set 0.3.0 (active) | The Safe is admin and nominated the timelock (batch executed 2026-10-17). The accept could be scheduled from 2026-10-20 08:42 UTC. The handbook records no completed accept as of 2026-10-24; npm run governance:v03:sepolia reads the current state |
The governance screen at /admin shows the timelock address, its delay, whether the handover is finished, and the queue of operations (src/services/domain/platform-governance-service.ts, reading the last 50,000 blocks of timelock events).
A compromised pair of Safe owners can still schedule a hostile action. The delay is the window in which the monitor, institutions and the remaining owner can notice, and cancelling also needs the Safe's threshold. The project has not audited the timelock itself; it relies on the OpenZeppelin implementation and checks the configuration by test and by script.
Source files
blockchain/contracts/v03/CertichainTimelock.solblockchain/scripts/deploy-timelock.ts,blockchain/scripts/lib/timelock.tsblockchain/scripts/timelock-handover.ts,blockchain/scripts/lib/timelock-handover.ts,blockchain/scripts/lib/timelock-state.tsblockchain/scripts/nominate-timelock.ts,blockchain/scripts/lib/timelock-nominate.tsblockchain/scripts/timelock-propose.ts,blockchain/scripts/lib/timelock-proposal.ts,blockchain/scripts/lib/timelock-targets.ts,blockchain/scripts/lib/timelock-files.tsblockchain/scripts/verify-timelock.ts,blockchain/scripts/governance-status.ts,blockchain/scripts/admission-check.tsdocs/process/runbooks/timelock.md(the operating procedure)
Tests
blockchain/test/v03/Timelock.test.ts: roles, delay, scheduling, execution, cancellation, a batch with one failing call changing nothingblockchain/test/v03/TimelockDeploy.test.ts: the plan checks and the read-back of rolesblockchain/test/v03/TimelockHandover.test.ts: the next-step planner over every registryblockchain/test/v03/TimelockProposal.test.ts,TimelockTargets.test.ts: encoding, operation ids, Safe batch files, target resolutionblockchain/test/v03/Admission.test.ts,GovernanceLog.test.ts: sign-off salts and the governance log checkblockchain/test/v03/Governance.test.ts,Handover.test.ts,SafeGovernance.test.ts: two-step admin transfer, the Safe handover with a real Safe 1.5.0