Authorities
One role per extension, dispatched through a single function, so modules know nothing about the access-control scheme the assembly chose.
All docs
Every configuration entry point in every module routes through one internal function, passing its own extension identifier. The assembly implements that function once and dispatches on the identifier.
function _authorizeExtensionConfig(bytes4 extensionId) internal view override {
if (extensionId == ExtensionIds.TRANSFER_FEE) {
_checkRole(FEE_CONFIG_ROLE);
} else if (extensionId == ExtensionIds.TRANSFER_RESTRICTION) {
_checkRole(RESTRICTION_ROLE);
}
// ...
}This is what keeps per-extension authorities possible — a fee authority and a freeze authority that are different keys — without any module knowing what access-control scheme is in use. An assembly can use roles, a single owner, a timelock or a governor, and the modules are identical in every case.
Deliberately abstract
The function has no default implementation. An assembly must state its authorisation policy rather than inherit one that might be permissive. Forgetting is a compile error, not a live token with an open configuration surface.
The roles the reference tokens define
- MINT_ROLE
- Create supply.
- SEIZE_ROLE
- Burn from any account.
- METADATA_ROLE
- The key/value store and the token URI.
- FEE_CONFIG_ROLE
- Rate, cap, vault and exemptions.
- RESTRICTION_ROLE
- The global pause and per-account freezes.
- HOOK_CONFIG_ROLE
- The hook target and its gas budget.
- UPGRADER_ROLE
- Implementation upgrades, on the upgradeable variant only.
Why minting and seizing are separate
They were one role until the behaviour word learned to declare them separately. Once MINTABLE and SEIZABLE were two independent bits, a single key behind both meant the vocabulary drew a distinction the implementation could not hold — and a specification finer-grained than its own authorities is a specification that misleads.
Seizing is the larger power and deserves its own key. It burns from any account, including one whose holder never consented, and it exists because a freeze that also blocked burning could never be settled. It also covers a treasury retiring its own tokens: there is no permissionless self-burn here, so that treasury holds SEIZE_ROLE too.
Posture for a deployment others will integrate with
- The admin role
- On a multisig behind a timelock, never on an EOA. It is the role that can undo every other split, so every other decision here is downstream of this one.
- Each operational role
- On its own key. The deploy script does this, asserts it, and refuses to do it to the zero address.
- The upgrade role
- On a timelock too, or the UPGRADEABLE flag is a warning with no mitigation behind it. Or deploy a runtime clone, which has no upgrade path at all.
- A two-step handover
- AccessControl grants in one transaction, so a mistyped admin address is unrecoverable. The variant with a pending-accept step and a delay is worth swapping in for a deployment that matters.
The zero-address check has to live in the deploy script rather than in the token. The script initialises the token with the broadcaster so it can configure it before handing the roles over, so the token's own guard never sees the addresses that end up holding them — and granting a role to the zero address is accepted without complaint.