Skip to content

ERC7540 base ordering is C3-incompatible with ERC20uRWA and with core contracts like AccessControl #269

Description

@superken01

Summary

Two related findings, both verified at 07fb6f2 (current master):

1. ERC7540 linearizes ERC165 below Context/ERC20, the opposite of everything else.

ERC7540 declares is ERC165, ERC20, ..., which makes ERC165/IERC165 the most base contracts in its hierarchy — below Context (brought in by ERC20). Every other relevant contract puts ERC165 above Context/IERC20:

  • core: AccessControl is Context, ERC165, ..., ERC1155 is Context, ERC165, ..., TimelockController, interface IERC1363 is IERC20, IERC165
  • this repo: ERC20uRWA is ERC20, ERC165, ...

interface IERC7575 is IERC165, IERC4626 has the same inversion at the interface level (conflicts with IERC1363).

As a result, ERC7540 cannot be composed with ERC20uRWA — arguably an expected combination in this very repo (an async RWA vault whose share token is the vault itself, the single-contract ERC-7575 layout that share() supports) — nor with core AccessControl:

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ERC7540} from "@openzeppelin/community-contracts/token/ERC20/extensions/ERC7540.sol";
import {ERC20uRWA} from "@openzeppelin/community-contracts/token/ERC20/extensions/ERC20uRWA.sol";

// Error (5005): Linearization of inheritance graph impossible
abstract contract URWAAsyncVault is ERC7540, ERC20uRWA {}

// Error (5005): Linearization of inheritance graph impossible
abstract contract ManagedAsyncVault is AccessControl, ERC7540 {}

Both fail with either child ordering, since the contradiction lives in the parents' own is lists:

contract relevant linearization (derived → base)
ERC7540 ... ERC20 ... Context > ERC165 > IERC165
ERC20uRWA ... ERC165 > IERC165 > ERC20 ... Context
AccessControl ... ERC165 > IERC165 > Context

2. The CI check meant to catch this has been silently checking nothing.

scripts/checks/inheritance-ordering.js still reads its arguments with require('yargs/yargs')().argv. With the yargs v18 in the lockfile, a bare yargs() no longer defaults to process.argv, so artifacts is always [], the loop never runs, and the check prints Contract ordering is consistent. unconditionally. The copy in openzeppelin-contracts master was already migrated to yargs(hideBin(process.argv)):
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/scripts/checks/inheritance-ordering.js

Once the script actually runs, it flags the ERC7540/ERC20uRWA inconsistency reported above (as a 2-cycle between ERC20-side and ERC165-side nodes), plus a few pre-existing conflicts in the account area (RoleAccount vs the ordering used by Account* mocks, SignerEIP7702, TimelockController vs Account on ERC721Holder). I can file those separately if useful.

Suggested fix (verified locally)

-abstract contract ERC7540 is ERC165, ERC20, IERC4626, IERC7540, IERC7575Share {
+abstract contract ERC7540 is ERC20, IERC4626, IERC7540, IERC7575Share, ERC165 {
-interface IERC7575 is IERC165, IERC4626 {
+interface IERC7575 is IERC4626, IERC165 {

(ERC165 has to come after IERC7540/IERC7575Share because the fixed IERC7575 places IERC165 above IERC4626.)

With these two lines:

  • both compositions above compile (with the usual explicit supportsInterface/decimals/totalSupply overrides in the child);
  • no storage impact (ERC165 is stateless) and no behavior change (supportsInterface is explicitly overridden in ERC7540, so resolution order is unaffected);
  • the ERC7540Admin*/Delay*/Sync* strategy extensions inherit the fix automatically;
  • forge test passes, and the Hardhat suites for ERC7540/ERC20uRWA pass (384 passing);
  • the (fixed) inheritance check reports no remaining ERC165-related conflicts.

Happy to open a PR with the two-line reorder, and optionally the inheritance-ordering.js sync from openzeppelin-contracts master as a separate PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions