Access Controls

Cap uses a two-contract access control system. The AccessControl contract is the single authority that maps (function selector, contract address) pairs to addresses that are allowed to call them. The Access abstract contract is a thin mixin inherited by every Cap contract — it provides the checkAccess modifier that calls out to AccessControl to validate each privileged operation.

This architecture gives Cap fine-grained, per-function permissioning across the entire protocol with a single upgradeable registry.

Mechanics

Role Derivation

Every permission in the system is represented as an OpenZeppelin role — a bytes32 identifier. The role for a given (selector, contract) pair is computed as:

roleId = bytes32(selector) | bytes32(uint256(uint160(contract)));

The selector occupies the top 4 bytes; the contract address fills the lower 20 bytes. This means two functions with the same selector on different contracts get distinct roles automatically.

Granting and Revoking Access

grantAccess and revokeAccess are themselves access-controlled — only addresses holding the role(grantAccess.selector, AccessControl) role can modify permissions. The deployer (initial admin) receives:

  • DEFAULT_ADMIN_ROLE (OpenZeppelin admin role)

  • The role to call grantAccess on AccessControl

  • The role to call revokeAccess on AccessControl

Self-revocation of the revokeAccess role is blocked — an admin cannot accidentally lock themselves out by revoking their own revocation privilege.

checkAccess Modifier

Every privileged function in Cap contracts uses the checkAccess modifier:

_checkAccess calls IAccessControl.checkAccess(_selector, address(this), msg.sender) on the stored accessControl address. If the caller does not hold the required role, the call reverts with AccessDenied. There are no owner or operator patterns — all access is role-based.

The bytes4(0) selector is conventionally used for UUPS upgrade authorization (_authorizeUpgrade) across all upgradeable Cap contracts.


Architecture

AccessControl

The central permissions registry. UUPS upgradeable; upgrade requires DEFAULT_ADMIN_ROLE.

initialize(address _admin)

Bootstraps the contract. Grants DEFAULT_ADMIN_ROLE and both grantAccess / revokeAccess roles to _admin.


grantAccess(bytes4 _selector, address _contract, address _address)

Grants _address permission to call _selector on _contract. Requires the caller to hold the grantAccess role on this AccessControl contract.

Parameter
Type
Description

_selector

bytes4

Function selector being permissioned

_contract

address

Contract on which the function lives

_address

address

Address being granted access


revokeAccess(bytes4 _selector, address _contract, address _address)

Revokes _address's permission to call _selector on _contract. Cannot be used to revoke the caller's own revokeAccess role (reverts with CannotRevokeSelf).


checkAccess(bytes4 _selector, address _contract, address _caller)

Verifies that _caller holds the role for (_selector, _contract). Reverts (via OpenZeppelin _checkRole) if they do not. Returns true if access is granted.


role(bytes4 _selector, address _contract)

Computes the role identifier for a (selector, contract) pair. Pure function — can be called off-chain to compute role IDs for grantAccess / revokeAccess.


Access (mixin)

Inherited by every Cap contract that has privileged functions. Stores the accessControl address and exposes the checkAccess modifier.

__Access_init(address _accessControl)

Called from each contract's initialize. Stores the AccessControl contract address in ERC-7201 namespaced storage.


Usage Examples

1. Granting a keeper permission to call realizeInterest


2. Revoking a deprecated admin address


3. Timelock

Critical admin operations are executed through an OpenZeppelin TimelockControllerarrow-up-right deployed at 0xD8236031d8279d82E615aF2BFab5FC0127A329abarrow-up-right. The Timelock holds roles in Cap's AccessControl system like any other permissioned address — the difference is that all calls from the Timelock must first be scheduled and wait a minimum delay before execution.

Parameter
Value

Min Delay

86,400 seconds (1 day)

Type

OpenZeppelin TimelockController

The Timelock follows the standard OZ lifecycle:

  1. Scheduleschedule() or scheduleBatch() queues an operation with at least minDelay seconds of waiting time

  2. Wait — the operation cannot be executed until the delay has elapsed

  3. Executeexecute() or executeBatch() runs the queued operation

  4. Cancel (optional) — cancel() removes a pending operation before execution

For full interface details, see the OpenZeppelin TimelockController documentationarrow-up-right.


4. Checking access off-chain (role ID calculation)

Last updated