Map the authority before auditing a guard
OpenZeppelin Contracts v5.3.0 separates a manager that can dispatch calls from a target contract that can enforce its own restriction. The manager’s execute receives a target and encoded call, computes permission for the caller and selector, handles a required schedule, and calls the target. A target inheriting AccessManaged can put restricted on a function, invoking _checkCanCall before its body. These are distinct enforcement locations in the same architecture.
Scroll the diagram horizontally
The question mark is deliberate: the library does not automatically mark every function in an arbitrary target as restricted. Conversely, the manager’s path cannot be evaluated as a simple Boolean permission: execute distinguishes immediate and delayed calls, and the target-side check evaluates its own caller, target, and selector context. This source map establishes what the components can do, before asking whether a particular deployed target exposes an unguarded path to a privileged effect.
The research question that follows is about coverage: enumerate every entry point that reaches the same state change, then record where each route enforces the necessary condition. A visible check on one path is evidence for that path alone.
One visible route
OpenZeppelin Contracts v5.3.0 checks the caller, target, and call data in AccessManager.execute:
(bool immediate, uint32 setback) = _canCallExtended(caller, target, data);
The execute source, lines 492–520 rejects unauthorized calls, consumes a scheduled operation when required, and only then calls Address.functionCallWithValue(target, data, msg.value). This is direct evidence for the manager’s execution route. A separate AccessManaged.restricted modifier illustrates a check on a target entry point.
Trace both entry points
The source at v5.3.0 supports this call-flow annotation:
AccessManager.execute(target, data)
→ caller = _msgSender() // identify the caller
→ _canCallExtended(caller, target, data) // check manager-route permission
→ _consumeScheduledOp(...) when required // enforce an applicable delay
→ functionCallWithValue(target, data, ...) // call after those checks
AccessManaged.restricted on a target entry point
→ _checkCanCall(_msgSender(), _msgData()) // check the entering call
→ function body
The manager steps are visible in AccessManager.execute. The target-side check is visible in restricted and _checkCanCall. The manager also records the target and selector as the active execution before calling the target. A direct call to a restricted target enters through the target’s own modifier; the manager route first passes through execute. The trace does not establish that any particular target function is missing restricted.
The call graph is the boundary
“This operation is guarded” is incomplete until its route is named. A check in a manager protects calls through that manager; a check on the target can protect calls that arrive by other paths. An architecture may use either boundary deliberately. The question for a reviewer is which entry points can reach the state change and what each path promises to check.
Authorization can live at either point, making the call graph the useful unit of review.
Route matrix
Start from the state-changing function and enumerate its external callers, internal dispatchers, callbacks, and scheduled routes. For each path, record the caller identity and the check that precedes the state change. If a path is intentionally trusted, document where that trust is established. A test of a proposed gap needs the same initial state on a control route and the questioned route, plus the committed state after each call.
Separate authorization from the protected effect
The manager route has more structure than a single Boolean. execute at v5.3.0 obtains (immediate, setback) for the caller and target call data. It reverts when neither immediate permission nor a delay exists; it consumes a scheduled operation when required. Only after those checks does it record an execution identifier for the target and selector and call the target:
_executionId = _hashExecutionId(target, _checkSelector(data));
Address.functionCallWithValue(target, data, msg.value);
_executionId = executionIdBefore;
The target-side restricted modifier calls _checkCanCall before its function body. That check asks the configured authority about caller, address(this), and the first four calldata bytes; it either permits immediately, consumes an applicable scheduled operation, or reverts (AccessManaged.sol, lines 92–112). These source paths show how a library can put checks at both a dispatch route and a target entry point. They do not tell us that every function of an arbitrary target uses the modifier.
The unit of review is each path to a sink
Suppose a protected state change has two external entry points, A and B, both calling a shared internal sink S. As a synthetic model, if A checks a necessary precondition and B does not, then observing a successful check on A says nothing about calls through B. The strongest proof is not a screenshot of a guard; it is a route matrix tied to execution evidence:
A route matrix should put A and B on separate rows and record the caller identity, authorization decision, and committed state for each.
Document whether the sink enforces its own invariant, whether a wrapper checks it, and whether an intentional trusted route has a separate guarantee. A differential fixture should use identical starting state and action data, vary only the entry route, and record both the authorization result and final state. If B is impossible to reach under the actual deployment, or if S enforces the condition internally, a missing check in one wrapper is not a demonstrated vulnerability.
“The manager checks access” describes manager-mediated calls. “The target cannot be reached without the check” requires the full call graph, modifier coverage, and deployed configuration.
Eliminate the easy false positives
An unmodified target function is not necessarily an unguarded privileged action. It may only call a sink that enforces the condition internally, be reachable only through a trusted dispatcher, or make a state change whose security predicate differs from a neighboring function’s. Conversely, finding restricted on one method does not establish that every sibling method with a similar name uses it. The analysis starts at the effect, then walks backward through all paths that can cause it.
The manager’s active execution identifier is another reason to avoid a simplistic “one guard versus two guards” account. A target’s authority query can distinguish the manager-mediated execution from an ordinary direct call. The manager also consumes scheduled operations under the conditions shown in execute; a delayed permission cannot be summarized as an unconditional allow. A route matrix should therefore include the effective caller, target selector, required delay or schedule, target modifier, and final state-changing sink. Leaving out the selector can be fatal to the analysis because two methods on the same target can have different permissions.
For a publishable finding, the minimum record is a reachable sibling route that fails to enforce the same necessary condition, an input that satisfies all other preconditions, a control route that rejects it, and a state or asset difference after execution. If a supposed bypass route reverts for another reason, or if the shared sink checks the condition anyway, the route asymmetry is only a code-review observation. This standard comes from the call graph, not from the visual presence or absence of a modifier.