Skip to main content

ADR-025: Pause

StatusProposed
Date2026-05-20

Context

The contracts have no kill switch. A discovered bug in a verifier, a compromised minter key, or an exploit in progress currently has no response short of an upgrade — slow, owner-multisig-gated, and broadcast on-chain in advance. The issuer needs a circuit breaker that halts user-driven balance movement immediately while leaving admin paths (freeze, seize, role changes, upgrade, key rotation) available so the situation can be triaged.

Pause is deployment-wide in the common case (suspected systemic issue) but occasionally token-local (a single EBEMT's underlying depegs, a token-specific verifier issue). Forcing the owner to pause every EBEMT individually is the wrong default; forcing global-only pause throws away legitimate isolation when one token has a localized problem.

Proposal

Pause state lives entirely on the Hub. Two flags, one global and one per-EBEMT:

bool public paused; // global
mapping(address => bool) public pausedEbemts; // per-EBEMT
mapping(address => bool) public pausers; // role

A user-facing operation on EBEMT T is blocked if Hub.paused() || Hub.pausedEbemts(T). EBEMTs read both with a single Hub view (isPaused(address ebemt) → bool) to keep the hot-path cost to one external call.

Role

A new owner-maintained role set, disjoint from minters, freezers, seizers. owner is implicitly a pauser — every pause / unpause entry point treats msg.sender == owner as authorized without requiring a registry entry, so the owner key alone is always sufficient to halt the system in an emergency. The owner additionally has full control over the pausers set and can enroll faster-response operational addresses (typically a hot multisig) to act without going through the owner key.

function setPauser(address pauser, bool enabled) external onlyOwner; // immediate

Unlike freezer / seizer, where owner is deliberately not implicitly enrolled (the ADR-022 separation-of-duties argument), pause is a coarse reversible circuit breaker with no fund-movement consequence, so collapsing it onto the owner key is acceptable and operationally useful.

Entry points (all pausers[msg.sender]-gated)

function pause() external; // global
function unpause() external;
function pauseEbemt(address ebemt) external; // per-EBEMT
function unpauseEbemt(address ebemt) external;

pause() and pauseEbemt(T) are independent — clearing one does not clear the other. The owner can therefore globally pause for an incident, locally pause a known-bad EBEMT before lifting the global pause, then unpause globally without re-enabling the bad token.

Enforcement

Every user-facing entry point checks Hub.isPaused(ebemt) and reverts with ContractIsPaused(ebemt) if set. Admin paths skip the check.

On EBEMT: publicToEncryptedTransfer*, encryptedToPublicTransfer*, encryptedTransfer*, activatePending*, setAutoEncrypt*, toggleAutoEncrypt*, and the ERC-20 _update chokepoint (covering transfer, transferFrom, permit, ERC-3009). onlyHub executors are not gated — pause is enforced at the Hub entry point that calls them.

On EBHub: publicMint, publicBurn, encryptedMint, encryptedBurn, registerEpk, changeControllerWithAuth. The mint/burn checks take an ebemt parameter and use it for the per-EBEMT lookup; identity-registry calls (registerEpk, changeControllerWithAuth) only consult the global flag, since they are not bound to any specific EBEMT.

Explicitly not gated: freeze, unfreeze, seize, all role setters, setSanctionsList, setTrcPk, rotateTotalSupplyKey, upgradeEbemt, registerEbemt. These remain available during pause — without them, pause is useless for incident response.

Events

EventPurpose
Paused() / Unpaused()Global flag changes
EbemtPaused(address ebemt) / EbemtUnpaused(address ebemt)Per-EBEMT flag changes
PauserUpdated(address indexed pauser, bool enabled)Admin role change

Consequences

Easier. Single-call global halt of every EBEMT in the deployment, with surgical per-EBEMT pause as an alternative when an incident is localized. Pause state co-located with the rest of the issuer-wide levers (freeze registry, sanctions oracle, total-supply key) — one mental model, one place to audit. New EBEMTs registered later inherit the global flag for free.

Harder. Every user-facing op on EBEMT picks up one extra Hub call (isPaused) on top of the existing freeze/sanctions reads — negligible gas, but one more dependency on Hub liveness. A Hub-side bug that flips paused accidentally halts the whole deployment; the pauser role belongs on a different multisig from owner so a single key compromise cannot both pause and prevent unpause via upgrade. Two flags means two states to monitor off-chain; indexers must track both events to render an accurate paused-status per token.