TransparentUpgradeableProxy.sol and ProxyAdmin.sol form a circular import because ITransparentUpgradeableProxy is defined in the same file as TransparentUpgradeableProxy. Hardhat flatten fails with HH603 on either file.
💻 Environment
@openzeppelin/contracts ≥ 5.0 (introduced in #4382)
- Hardhat 2.28.5 (
HH603); Hardhat 3 has the same limitation (HHE602 / FLATTEN_CYCLIC_DEPENDENCY)
📝 Details
ProxyAdmin imports the interface from the proxy file:
import {ITransparentUpgradeableProxy} from "./TransparentUpgradeableProxy.sol";
Since v5, TransparentUpgradeableProxy also imports ProxyAdmin:
import {ProxyAdmin} from "./ProxyAdmin.sol";
This creates a cycle:
ProxyAdmin.sol
→ ITransparentUpgradeableProxy (in TransparentUpgradeableProxy.sol)
→ ProxyAdmin.sol
Solidity and forge flatten accept this. Hardhat does not, so npx hardhat flatten on either file fails with HH603. That blocks single-file flattening for verification workflows.
Extracting ITransparentUpgradeableProxy to its own file removes the cycle (see draft implementation in #6653), although breaks for code that currently imports the interface from TransparentUpgradeableProxy.sol.
🔢 Code to reproduce bug
npx hardhat flatten contracts/proxy/transparent/TransparentUpgradeableProxy.sol
npx hardhat flatten contracts/proxy/transparent/ProxyAdmin.sol
Error HH603: Hardhat flatten doesn't support cyclic dependencies.
TransparentUpgradeableProxy.solandProxyAdmin.solform a circular import becauseITransparentUpgradeableProxyis defined in the same file asTransparentUpgradeableProxy. Hardhatflattenfails withHH603on either file.💻 Environment
@openzeppelin/contracts≥ 5.0 (introduced in #4382)HH603); Hardhat 3 has the same limitation (HHE602/FLATTEN_CYCLIC_DEPENDENCY)📝 Details
ProxyAdminimports the interface from the proxy file:Since v5,
TransparentUpgradeableProxyalso importsProxyAdmin:This creates a cycle:
Solidity and
forge flattenaccept this. Hardhat does not, sonpx hardhat flattenon either file fails withHH603. That blocks single-file flattening for verification workflows.Extracting
ITransparentUpgradeableProxyto its own file removes the cycle (see draft implementation in #6653), although breaks for code that currently imports the interface fromTransparentUpgradeableProxy.sol.🔢 Code to reproduce bug