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.
Summary
Two related findings, both verified at
07fb6f2(current master):1.
ERC7540linearizesERC165belowContext/ERC20, the opposite of everything else.ERC7540declaresis ERC165, ERC20, ..., which makesERC165/IERC165the most base contracts in its hierarchy — belowContext(brought in byERC20). Every other relevant contract putsERC165aboveContext/IERC20:AccessControl is Context, ERC165, ...,ERC1155 is Context, ERC165, ...,TimelockController,interface IERC1363 is IERC20, IERC165ERC20uRWA is ERC20, ERC165, ...interface IERC7575 is IERC165, IERC4626has the same inversion at the interface level (conflicts withIERC1363).As a result,
ERC7540cannot be composed withERC20uRWA— 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 thatshare()supports) — nor with coreAccessControl:Both fail with either child ordering, since the contradiction lives in the parents' own
islists:ERC7540ERC20...Context>ERC165>IERC165ERC20uRWAERC165>IERC165>ERC20...ContextAccessControlERC165>IERC165>Context2. The CI check meant to catch this has been silently checking nothing.
scripts/checks/inheritance-ordering.jsstill reads its arguments withrequire('yargs/yargs')().argv. With the yargs v18 in the lockfile, a bareyargs()no longer defaults toprocess.argv, soartifactsis always[], the loop never runs, and the check printsContract ordering is consistent.unconditionally. The copy inopenzeppelin-contractsmaster was already migrated toyargs(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/ERC20uRWAinconsistency reported above (as a 2-cycle betweenERC20-side andERC165-side nodes), plus a few pre-existing conflicts in the account area (RoleAccountvs the ordering used byAccount*mocks,SignerEIP7702,TimelockControllervsAccountonERC721Holder). I can file those separately if useful.Suggested fix (verified locally)
(
ERC165has to come afterIERC7540/IERC7575Sharebecause the fixedIERC7575placesIERC165aboveIERC4626.)With these two lines:
supportsInterface/decimals/totalSupplyoverrides in the child);ERC165is stateless) and no behavior change (supportsInterfaceis explicitly overridden inERC7540, so resolution order is unaffected);ERC7540Admin*/Delay*/Sync*strategy extensions inherit the fix automatically;forge testpasses, and the Hardhat suites for ERC7540/ERC20uRWA pass (384 passing);ERC165-related conflicts.Happy to open a PR with the two-line reorder, and optionally the
inheritance-ordering.jssync fromopenzeppelin-contractsmaster as a separate PR.