EVM patterns
ERC165 Interface Detection: What a Positive Result Proves
A support response advertises a callable interface. Behavior and authorization need separate evidence. ERC165 interface detection should use a named detector policy, retain unresolved outcomes and inform only the integration decision that its evidence supports.

Key facts
- Original comparison
- 216 complete symbolic response profiles. No deployed addresses sampled
- Checked helper transcription
- 16 accepted and 200 rejected profiles in the fixed domain
- Canonical diagnostic policy
- 1 accepted profile. This is a stricter comparison, not a patch recommendation
- Source scope
- OpenZeppelin Contracts v5.4.0 source predicates. Python enumeration, no EVM execution
- Decision boundary
- Interface claims do not establish behavior or authorization
Separate interface claims from integration evidence
A positive response is an observation about a call. It does not establish that the advertised operation preserves balances, rejects unauthorized callers or produces the state your application expects. ERC165 interface detection helps select a compatible interaction path. The integration still needs evidence for the action it will allow.
Our finite comparison makes that distinction visible. A target-only predicate accepted 72 of 216 constructed response profiles. A transcription of the checked helper in OpenZeppelin Contracts v5.4.0 accepted 16. A deliberately stricter canonical-response policy accepted 1.
These are counts over a declared input domain, not measurements of detection accuracy or contracts on a network.
The interesting unit is the response profile. It combines what a hypothetical address returns for the ERC165 identifier, the reserved invalid identifier and a distinct requested interface. No real address was sampled. There is no independently observed implementation behind these answers, so the comparison cannot label an accepted profile a genuine implementation or a vulnerability.
| Question | Useful evidence | What remains unresolved |
|---|---|---|
| Does this address advertise the requested interface? | Recorded probe responses under a named detector policy | Whether the implementation performs the advertised behavior |
| Can this caller perform the operation? | Authorization checks and tests using the actual caller context | Whether all permitted state transitions preserve application rules |
| Will this integration account for the result correctly? | Before and after state assertions with rejection controls | Behavior outside the exercised state and assumptions |
Use this distinction when reviewing a plugin, receiver or token adapter. A route selector can reasonably use an advertised capability. Giving that address spending authority requires a separate decision. Write down which decision the probe informs before interpreting its result. The smart contract threat modeling guide provides the wider trust-boundary exercise.
Original comparison across 216 response profiles
Method
We executed a standard-library Python enumeration on . The script transcribes the relevant predicates from a fetched source snapshot. It does not compile Solidity or execute an EVM. The three probes have distinct identifiers, so one response slot does not stand for repeated calls to the same selector argument.
- Source snapshot
- OpenZeppelin Contracts
v5.4.0, commitc64a1edb67b6e3f4a15cca8909c9482ad33a02b0, specificallyERC165Checker.sol. The commit identifies the selected release. This is not a claim that this is the latest version. - Population
- All combinations of 6 fixed response classes at each of 3 probe positions:
6 × 6 × 6 = 216profiles, with 0 exclusions. - Probe positions
- The ERC165 self identifier
0x01ffc9a7, invalid identifier0xffffffffand a separate target interface identifier. - Artifacts
seo/research/erc165-interface-detection-2026-11-04/measurement.py,measurement.jsonandresponse-profiles.csvare retained in the repository. These paths are evidence locations, not public downloads.
Each slot has a fixed outcome. Successful responses contain a 32-byte word equal to 0, 1 or 2, 31-byte zero data or empty data. The remaining response class is a failed call with no data. We label the word 2 nonboolean because the Solidity ABI specification restricts boolean values to 0 and 1. Short, empty and failed responses remain distinct raw classes even when a predicate maps them to the same answer.
Results
| Policy | Accept | Reject | Meaning |
|---|---|---|---|
| Target-only unchecked predicate | 72 / 216 | 144 / 216 | Only the requested interface response is evaluated |
| Pinned checked-helper transcription | 16 / 216 | 200 / 216 | Positive self and target predicates, negative invalid-ID predicate |
| Canonical diagnostic policy | 1 / 216 | 215 / 216 | Successful canonical true, false and true answers respectively |
The checked-helper transcription and the canonical comparator differ on 15 profiles. Among the 16 profiles the former accepts, 12 have a short, empty or failed invalid-ID response. Also, 12 have a nonboolean positive response at the self or target position. Those groups overlap in 9 profiles. They cannot be added as independent findings.
This pattern follows from the source expression. The unchecked helper treats a call as positive when it succeeds, returns at least 32 bytes and has a first word greater than zero. A failed or short invalid-ID probe therefore becomes a negative predicate. Negating that predicate satisfies that part of the checked helper. In contrast, the standard's detection procedure explicitly rejects a failed invalid-ID call.
The canonical comparator preserves the distinction between an explicit successful false answer and an unsuccessful observation. It also rejects the constructed word 2. This is a diagnostic comparison chosen for the study. It is not an OpenZeppelin patch recommendation, a demonstrated exploit or evidence that any deployed contract produces these answers.
Limits
- The domain contains representative response classes, not every possible return byte string.
- No compiler, bytecode, gas exhaustion or call-context behavior was executed. The Python result does not certify the transcription against an EVM trace.
- Profiles have fixed answers. Adaptive responses depending on the caller, state or earlier activity fall outside the comparison, as do oversized payload variants.
- Actual implementation behavior is unknown for every profile. An acceptance count cannot become a false-positive rate.
Reproduction requires the retained source files and Python. Run the script normally to regenerate the ledger, then use --verify to compare every result and source hash with the saved output. A changed predicate or input class defines a new experiment and needs a new denominator.
Read the detector and its preconditions
The word unchecked matters. OpenZeppelin documents supportsERC165InterfaceUnchecked as assuming that the account contains a contract supporting ERC165. Its behavior outside that precondition is undefined in the API contract. The documentation also warns that some precompiled contracts can falsely indicate support. Using the helper alone on an arbitrary address discards a documented condition.
The ordinary supportsInterface helper checks ERC165 support automatically before the target interface. In the pinned source, that support check asks for a positive ERC165 response and negates the invalid-ID result. Read the complete expression, including how the low-level probe converts errors.
The name of the outer helper does not tell you whether a negative internal boolean means explicit rejection, missing data or a failed call.
| Self / invalid / target | Target-only | Checked transcription | Canonical policy | Interpretation |
|---|---|---|---|---|
| Canonical: true / false / true | Accept | Accept | Accept | Canonical response pattern. Implementation behavior still unknown |
| Always positive: true / true / true | Accept | Reject | Reject | The invalid-ID control defeats a responder saying yes everywhere |
| Failed control: true / revert / true | Accept | Accept | Reject | A failed invalid-ID observation is collapsed to false by the inner predicate |
| nonboolean / false / true | Accept | Accept | Reject | A positive return word is broader than a canonical encoded boolean |
Do not erase the raw outcome in diagnostic tooling. Store success, return length and the relevant returned bytes alongside the interpreted result. This recommendation concerns evidence retention: a production boolean API may intentionally provide a simpler answer. A reviewer needs both levels to explain a disagreement between tools.
If two detectors disagree, compare their policy before blaming the target. Reproduce the same requested identifier and preserve each raw outcome under the same recorded context. A difference in how the wrappers interpret missing data may explain the disagreement. If the underlying observations differ, the response-policy ledger alone cannot resolve the cause.
Gas also needs its proper scope. ERC165 specifies a support query within 30,000 gas, and the pinned helper requests that amount for its static call. That is not a measured budget for your entire transaction, nor proof that a nested caller has enough gas available. The present enumeration supplies response outcomes directly and measures no gas. If gas availability matters, exercise the actual call path and retain the execution trace.
A negative answer should stop a route that requires positive discovery evidence. It does not prove that the target lacks every relevant function. Keep a separate unsupported or unresolved outcome for the product decision, rather than assigning a security rating to the address.
Derive the interface identifier correctly
Two definitions sit close together but answer different questions. The ABI defines a function selector from its signature. ERC165 combines function selectors into an interface identifier using XOR. Return types do not participate in Solidity's function-overload resolution. The ABI JSON still records outputs, so preserve it when checking that your decoder expects the right result.
An identifier is consequently a narrow description. It does not encode all the semantic conditions an application attaches to an operation. A matching advertised identifier cannot establish that a permission check exists or that a successful call produces the intended economic result. Those properties need their own assertions.
Solidity adds a specific inheritance boundary: type(I).interfaceId includes functions defined in the interface itself and excludes inherited functions. If the integration requires a parent interface as well as a child, document the required identifiers explicitly. A test based on an assumed flattened set can disagree with the compiler while both sides use the same interface name.
- Freeze the interface source and compiler context used by the integration. Preserve the ABI, including output types.
- Derive the identifier from the intended interface definition. Record whether a parent interface requires a separate support claim.
- Inspect the implementation's override. The OpenZeppelin base advertises IERC165. Its documented extension pattern adds the new identifier while preserving
super.supportsInterface. - Test the requested identifier and the invalid control through the same wrapper your application will use. Keep expected answers with the source revision.
Changing an interface name in a document does not change its callable selector set. Conversely, preserving a recognizable name does not make a changed signature compatible. During review, compare the actual interface artifact with the call site. The dependency risk guide covers how to keep that artifact tied to a reviewed dependency revision.
A generic selector calculator is insufficient evidence if its hashing or signature rules differ from Ethereum's ABI rules. Use a verified toolchain for derivation and retain its output. We did not execute identifier generation in this study. The identifiers printed here come from the fetched standards and the pinned source.
Test the behavior behind the advertised interface
Passing discovery is the beginning of a behavioral test. Build a fixture that advertises the expected interface but rejects the operation, then another that accepts the call while violating the application's expected state change. These are proposed integration controls. They were not executed as part of the response enumeration.
- Discovery gate
- Interpret recorded responses using the named detector and its preconditions.
- Behavior gate
- Exercise the operation with explicit state assertions and rejecting controls.
- Action decision
- Combine those results with the caller's permission and the application's acceptance criteria. Unresolved evidence holds the dependent action.
Receiver standards make the distinction concrete. ERC721 specifies a receipt callback and a required magic return value. ERC1155 has its own receiver interface and acceptance rules. A successful support query does not substitute for the required callback outcome. The ERC1155 receiver security analysis separates transfer payloads, ledger effects and receiver acceptance.
Check the exact standard before reusing a gas assumption. ERC1155's receiver support query has a 10,000-gas bound, narrower than ERC165's general 30,000-gas figure. These values constrain particular specified queries. They are not interchangeable transaction limits, and neither was benchmarked in our Python exercise.
| Control | Required observation | Decision supported |
|---|---|---|
| Advertised interface, rejected operation | The integration handles the rejection without recording a successful application action | Whether failure handling depends incorrectly on the earlier support answer |
| Advertised interface, unexpected state change | The application checks the state property it actually requires, rather than only call success | Whether the semantic acceptance condition is enforced |
| Expected operation, unauthorized caller | The protected action is rejected even though interface discovery succeeds | Whether capability discovery has accidentally become permission |
| Valid caller and expected transition | The intended integration path completes and its state assertions hold | Whether the rejection controls have merely disabled the useful path |
For a practical acceptance test, identify the state that makes the operation meaningful. An adapter may need a particular balance delta. A receiver may need to reject an unsupported token. An administrative integration needs the actual caller's permission checked. Pair the positive path with a case that advertises the same capability but must not receive the same privilege. Preserve both outcomes rather than a single green discovery assertion.
Record the decision and its validity window
An interface result needs a context. Record the chain, address, relevant block reference, detector version and raw response evidence. Add the integration decision that depended on the result. This proposed record lets a later reviewer distinguish changed behavior from a different query or tool policy.
Where implementation changes are possible, define what invalidates the earlier assessment. A prior support response does not itself constrain future code or configuration. For an upgradeable target, include implementation review and change handling in the integration requirements. The upgradeable contract audit guide addresses that broader review.
Agree on a response to observation failures before enabling the integration. A diagnostic client that cannot obtain return bytes should mark that evidence unavailable. It should not manufacture an explicit false answer in its record. The dependent action can remain on hold while the owner distinguishes a target rejection from an observation problem. This is an operating policy, not an assertion that every RPC provider exposes the same error detail.
Retain the expected positive control with the rejected cases. Otherwise a blanket failure can look like successful enforcement. When a new implementation or detector revision is introduced, rerun the controls whose assumptions changed and compare the raw outcomes with the prior record. Do not preserve an old compatibility label simply because the address or product name stayed the same.
A third-party review can help evaluate the implementation, but the existence of a report is separate from its scope. Compare the reviewed revision and relevant findings with the contract your application uses. The Pharos Production member profile is a starting point for reviewing provider information, while the member directory supports that selection process. Neither replaces evidence about a particular deployment.
The finite ledger has a narrower use: it reveals what each declared predicate accepts within its chosen response classes. Update it when the source predicate or diagnostic policy changes. To make a claim about deployed behavior, add an execution record tied to the actual target and the state assertions that determine your integration's acceptance.
Frequently asked questions
When should I request per-interface batch results instead of one all-interface answer?
Use getSupportedInterfaces when optional capabilities need separate decisions. Its returned boolean array corresponds to the requested identifiers. supportsAllInterfaces instead returns one aggregate answer, so it does not identify which requested capability failed.
What does an empty interface array establish?
In the pinned source, supportsAllInterfaces still checks ERC165, then returns true if that check passes and there are no target identifiers to reject. An empty list therefore establishes no requested application capability. Validate that the caller actually supplied the requirements it intended to test.
Does the pinned unchecked predicate require exactly 32 return bytes?
It requires at least 32 bytes, along with call success and a positive first word. Extra bytes are not rejected by that length condition alone. The finite comparison here includes no oversized response classes, so it provides no execution evidence about their handling.