Skip to main content

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​

SettingValueWhy
ProposerThe admin Safe (2 of 3 on Sepolia) onlyOnly the Safe can schedule
CancellerThe Safe (OpenZeppelin grants it with the proposer role)The Safe can stop a scheduled action it regrets or that was forced
ExecutorThe zero address, meaning anyoneThe action was approved when it was scheduled; running it needs no extra trust
AdminNoneThe timelock administers itself: changing its delay or roles is itself a scheduled action
Delay1 hour on Sepolia, 2 days planned for productionA 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 timelockStays instant
addIssuer, removeIssuer, setIssuerName, retireIssuerGuardian 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 registryEvery 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)​

ItemState
TimelockDeployed 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).

Limits of a timelock

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.sol
  • blockchain/scripts/deploy-timelock.ts, blockchain/scripts/lib/timelock.ts
  • blockchain/scripts/timelock-handover.ts, blockchain/scripts/lib/timelock-handover.ts, blockchain/scripts/lib/timelock-state.ts
  • blockchain/scripts/nominate-timelock.ts, blockchain/scripts/lib/timelock-nominate.ts
  • blockchain/scripts/timelock-propose.ts, blockchain/scripts/lib/timelock-proposal.ts, blockchain/scripts/lib/timelock-targets.ts, blockchain/scripts/lib/timelock-files.ts
  • blockchain/scripts/verify-timelock.ts, blockchain/scripts/governance-status.ts, blockchain/scripts/admission-check.ts
  • docs/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 nothing
  • blockchain/test/v03/TimelockDeploy.test.ts: the plan checks and the read-back of roles
  • blockchain/test/v03/TimelockHandover.test.ts: the next-step planner over every registry
  • blockchain/test/v03/TimelockProposal.test.ts, TimelockTargets.test.ts: encoding, operation ids, Safe batch files, target resolution
  • blockchain/test/v03/Admission.test.ts, GovernanceLog.test.ts: sign-off salts and the governance log check
  • blockchain/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