Skip to content

Conversation

@ernestognw
Copy link
Member

@ernestognw ernestognw commented Aug 22, 2025

Fixes #5878

PR Checklist

  • Tests
  • Documentation
  • Changeset entry (run npx changeset add)

Summary by Sourcery

Improve ERC165Checker by introducing a low-level call helper to distinguish between call failures and false responses, update interface support logic to handle reverts and malformed returns, refactor related code and mocks, and enhance tests with async-await and edge-case scenarios

New Features:

  • Add trySupportsInterface helper to return both call success and interface support status

Bug Fixes:

  • Fix supportsERC165 to correctly handle invalid interface checks and revert scenarios
  • Ensure supportsERC165InterfaceUnchecked only returns true when the staticcall succeeds and returns valid data

Enhancements:

  • Refactor supportsERC165InterfaceUnchecked to use trySupportsInterface
  • Consolidate multiple ERC165 mock contracts into a single ERC165Mock.sol file
  • Update import path for IERC165 in mocks

Tests:

  • Convert all ERC165Checker tests to async-await with chai-as-promised syntax
  • Add tests covering contracts that revert, return malformed data, or omit return values in supportsInterface

@changeset-bot
Copy link

changeset-bot bot commented Aug 22, 2025

⚠️ No Changeset found

Latest commit: 7416347

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Aug 22, 2025

Reviewer's Guide

This PR refines ERC-165 detection by introducing a low-level trySupportsInterface helper in ERC165Checker, refactors existing checks to use it, adds edge-case mock contracts, updates tests to promise-aware assertions with new scenarios, and includes minor import path and documentation fixes.

Entity relationship diagram for ERC165Checker and mock contracts

erDiagram
    ERC165Checker ||--o| ERC165MaliciousData : checks
    ERC165Checker ||--o| ERC165MissingData : checks
    ERC165Checker ||--o| ERC165NotSupported : checks
    ERC165Checker ||--o| ERC165ReturnBombMock : checks
    ERC165Checker ||--o| ERC165RevertInvalid : checks
    ERC165ReturnBombMock ||--|{ IERC165 : implements
    ERC165RevertInvalid ||--|{ IERC165 : implements
Loading

Class diagram for new and updated ERC165 mock contracts

classDiagram
    class ERC165MaliciousData {
        +supportsInterface(bytes4): bool
    }
    class ERC165MissingData {
        +supportsInterface(bytes4)
    }
    class ERC165NotSupported {
    }
    class ERC165ReturnBombMock {
        +supportsInterface(bytes4): bool
    }
    class ERC165RevertInvalid {
        +supportsInterface(bytes4): bool
    }
    ERC165ReturnBombMock --|> IERC165
    ERC165RevertInvalid --|> IERC165
Loading

File-Level Changes

Change Details Files
Introduce trySupportsInterface and refactor ERC165Checker
  • Add trySupportsInterface function performing low-level staticcall with assembly
  • Refactor supportsERC165 to use trySupportsInterface for invalid interface check
  • Simplify supportsERC165InterfaceUnchecked to delegate to trySupportsInterface
  • Update function documentation with call success and support status details
contracts/utils/introspection/ERC165Checker.sol
Add edge-case ERC165 mock contracts and update imports
  • Introduce ERC165MaliciousData, ERC165MissingData, ERC165NotSupported, ERC165ReturnBombMock, and ERC165RevertInvalid mocks
  • Merge separate mock files into a single ERC165Mock and remove deleted files
  • Update IERC165 import path in mock contracts
contracts/mocks/ERC165Mock.sol
Update tests to promise-aware assertions and add new scenarios
  • Replace synchronous expect(await ...).to.be.x assertions with await expect(...).to.eventually.be.x
  • Add a dedicated 'ERC165 revert on invalid interface' test suite
  • Extend coverage using new edge-case mocks
test/utils/introspection/ERC165Checker.test.js
Apply minor documentation and import path fixes
  • Correct import paths for IERC165 references
  • Fix small typos in contract documentation comments
contracts/mocks/ERC165Mock.sol

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ernestognw ernestognw changed the title Inaccuracies Fix ERC165Checker detection Aug 22, 2025
@ernestognw ernestognw marked this pull request as ready for review August 22, 2025 22:33
@ernestognw ernestognw requested a review from a team as a code owner August 22, 2025 22:33
@arr00 arr00 linked an issue Aug 22, 2025 that may be closed by this pull request
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `contracts/utils/introspection/ERC165Checker.sol:123` </location>
<code_context>
+     * * `success`: true if the call didn't revert, false if it did
+     * * `supported`: true if the call succeeded AND returned data indicating the interface is supported
+     */
+    function trySupportsInterface(
+        address account,
+        bytes4 interfaceId
+    ) internal view returns (bool success, bool supported) {
         uint256 returnSize;
         uint256 returnValue;
+        bytes memory encodedParams = abi.encodeCall(IERC165.supportsInterface, (interfaceId));
         assembly ("memory-safe") {
             success := staticcall(30000, account, add(encodedParams, 0x20), mload(encodedParams), 0x00, 0x20)
             returnSize := returndatasize()
             returnValue := mload(0x00)
         }
-
-        return success && returnSize >= 0x20 && returnValue > 0;
+        return (success, returnSize >= 0x20 && returnValue > 0);
     }
 }
</code_context>

<issue_to_address>
The assembly block assumes the return value is at memory position 0, which may not always be safe.

Using mload(0x00) may not correctly capture the return value if the returned data is less than 32 bytes or formatted differently. To ensure reliability, use returndatacopy to copy the returned data before loading it.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
        assembly ("memory-safe") {
            success := staticcall(30000, account, add(encodedParams, 0x20), mload(encodedParams), 0x00, 0x20)
            returnSize := returndatasize()
            returnValue := mload(0x00)
        }
=======
        assembly ("memory-safe") {
            success := staticcall(30000, account, add(encodedParams, 0x20), mload(encodedParams), 0x00, 0x20)
            returnSize := returndatasize()
            // Copy the returned data to memory position 0x00
            returndatacopy(0x00, 0x00, returnSize)
            // Load the first 32 bytes from memory position 0x00
            returnValue := mload(0x00)
        }
>>>>>>> REPLACE

</suggested_fix>

### Comment 2
<location> `contracts/utils/introspection/ERC165Checker.sol:135` </location>
<code_context>
         }
-
-        return success && returnSize >= 0x20 && returnValue > 0;
+        return (success, returnSize >= 0x20 && returnValue > 0);
     }
 }
</code_context>

<issue_to_address>
The check for returnValue > 0 may not strictly conform to ERC165, which expects a boolean true/false.

Using returnValue == 1 would ensure strict ERC165 compliance and prevent contracts returning other nonzero values from being incorrectly considered as supporting the interface.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
        return (success, returnSize >= 0x20 && returnValue > 0);
=======
        return (success, returnSize >= 0x20 && returnValue == 1);
>>>>>>> REPLACE

</suggested_fix>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Amxx
Copy link
Collaborator

Amxx commented Aug 25, 2025

IMO we should we add a changelog entry for this fix

success := staticcall(30000, account, 0x00, 0x24, 0x00, 0x20)
supported := and(
gt(returndatasize(), 0x1F), // we have at least 20 bytes of returndata
iszero(iszero(mload(0x00))) // the first 20 bytes of returndata are non-zero
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we checking x!=0 as opposed to x==1?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want anything that's not 0 to be interpreted as true. I think the rationale was to avoid the abi.decode to revert (see here). With assembly that might not be relevant anymore.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a contract returns 2, is that considered true according to the ERC? I'd argue no.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's technically correct because the ERC only distinguishes between 0 and 1 (false and true), so it's unclear how to treat the entire 32-byte value returned. I agree it's not ideal.

I would keep it for backwards compatibilty

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think its a big deal but anything other than 1 is definitely not true

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll break one of our tests, though

#5880 (comment)

I think we should weigh @Amxx opinion here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah sourcery beat me here. @Amxx seems to want to keep it as is.

Copy link
Collaborator

@Amxx Amxx Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should consider all non-zero value as true, or if only 1 should be considered true. I guess both approach make sens.

What I'm sure is that so far (since OZ 2.0.0), the library assumed all non zero mean true. Changing that behavior is a breaking change. I would avoid that unless we have very strong reasons to change the behavior.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also agree it is not a big deal, though should we take the opportunity to update it in 6.0?

@ernestognw ernestognw requested a review from Amxx August 26, 2025 18:09
Co-authored-by: Hadrien Croubois <[email protected]>
Amxx
Amxx previously approved these changes Aug 27, 2025
@Amxx Amxx requested a review from arr00 August 27, 2025 07:43
success := staticcall(30000, account, 0x00, 0x24, 0x00, 0x20)
supported := and(
gt(returndatasize(), 0x1F), // we have at least 20 bytes of returndata
iszero(iszero(mload(0x00))) // the first 20 bytes of returndata are non-zero
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also agree it is not a big deal, though should we take the opportunity to update it in 6.0?

@Amxx Amxx merged commit 53bb340 into OpenZeppelin:master Aug 27, 2025
17 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ERC165Checker returns true on reverting invalid interface

4 participants