0.5.0 - 2026/08/26
Summary
The release that makes the factory's access control pluggable, and doubles the deployable surface as a result.
- Ten deployable factories instead of five. Each of the five families (UUPS, Transparent, Beacon, and the two CMTAT Light variants) now ships in two access-control flavours: the existing role-based
CMTAT_*_FACTORY, and a new single-ownerCMTAT_*_FACTORY_Ownable2Step. They share identical deployment logic and differ only in who may calldeployCMTAT. The choice is made at deployment and cannot be changed at a deployed address. deployCMTATis gated by a hook, not a hard-wired role.CMTATFactoryRootno longer inheritsAccessControl; it declaresonlyCMTATDeployerand a bodyless_authorizeDeployCMTAT(), which each deployment answers with a single modifier.CMTAT_DEPLOYER_ROLEmoved to the layer that enforces it, so anOwnable2Stepfactory never publishes a role it does not check.- A dependency-free integration interface.
ICMTATFactorydeclares the registry and salt surface every factory shares and imports nothing, so an indexer can compile against it without the CMTAT submodule or OpenZeppelin. isCustomSaltUsed(bytes32)closes a gap where the address predictors kept answering for a one-time-use salt that had already been consumed.contracts/reorganised so a file's path says what it is:interfaces/,modules/{core,proxy,deployment,access}/,libraries/, and the deployable directories.- Dependencies moved forward: CMTAT
v3.3.0-rc1->v3.3.0-rc3, OpenZeppelin5.6.1->5.7.0, solc0.8.34->0.8.36.
Compatibility: the five existing factories keep their public constructor signatures and their ABI shape. Two things do change for integrators — type(ICMTATFactory).interfaceId (the interface dropped its access-control member) and the deployed CMTAT implementation bytecode (the vendored submodule moved).
Verification: 117 tests pass (42 at the start of the release), Slither and Aderyn report nothing to fix, and the style checker reports 0 violations across all 8 checks.
Added
ICMTATFactory(contracts/interfaces/ICMTATFactory.sol): the deployment-registry and salt surface every factory exposes identically -CMTATProxyAddress,cmtatsList,cmtatCounterId,useCustomSalt,nextDeploymentSalt,isCustomSaltUsedand theCMTATDeployedevent (no access-control member: that is policy, and policy is chosen per deployment). It has no imports, so an indexer or integrating contract can compile against it without the CMTAT submodule or OpenZeppelin; previously the only way to call a factory was to import the concrete contract and its whole dependency graph. It is inherited (so the compiler enforces the match, not a comment) and advertised through ERC-165supportsInterface. The deployment entrypoints are deliberately not declared: their shapes differ across the family, their arguments are CMTAT types, anddeployCMTATreturns the concrete proxy type rather thanaddress. Costs +26 bytes per factory and leaves the ABI shape unchanged. Finding J-3 of the v0.5.0 code-quality review; covered bytest/FactoryInterface.test.js.isCustomSaltUsed(bytes32 salt)on every factory: whether a custom salt has already been consumed, sodeployCMTAT(...)with it would revertCMTAT_Factory_SaltAlreadyUsed. PreviouslycustomSaltUsedwasinternal, and the address predictors keep answering for a consumed salt (the CREATE2 address is still correct, it just can no longer be reached) - so an integrator had no on-chain way to tell a live prediction from a dead one short of sending a transaction and watching it revert. Returnsfalsein counter mode, which never records a salt. Finding H-1 of the v0.5.0 code-quality review; covered bytest/CustomSalt.test.js.
Changed
- Bumped the factory version constant to
0.5.0(ContractVersion.sol) and synced every mirror (package.json,package-lock.json, theversion()test,README.md,doc/README.md,AGENTS.md/CLAUDE.md). - Reordered functions and modifier keywords across the contracts to follow the Solidity style guide (constructor / external / public / internal / private,
viewandpurelast; visibility before mutability beforevirtual/overridebefore custom modifiers). Member moves only, no logic change. .gitignore: ignore LibreOffice lock files (.~lock.*#).- Reorganised
contracts/so a file's path says what it is.contracts/libraries/had grown to hold 13 abstract contracts and a single actuallibrary; the abstract contracts moved tocontracts/modules/, grouped by capability -core/(registry, salt logic, CREATE2, version),proxy/(proxy-mechanism bases),deployment/(per-family entrypoints) andaccess/(who may deploy) - leavinglibraries/holding onlyFactoryErrors. Two interfaces that were declared inside the module implementing them were extracted:IERC8303fromContractVersion.solandIERC173fromCMTATFactoryOwnable2Step.sol, both now undercontracts/interfaces/. The layout follows the upstream CMTAT convention. Behaviour-preserving: the moved files differ only in their import lines, every relative import was recomputed and verified to resolve, and all 117 tests pass. Finding J-1 of the v0.5.0 code-quality review. - Extracted the CREATE2 address prediction into
_computeCreate2Address(bytecode, salt)onCMTATFactoryRoot, replacing three byte-identical copies ofCreate2.computeAddress(...)in the beacon base, the transparent base and the UUPS factory, and removing the three now-unusedCreate2imports. It sits beside_deployAndRegisterProxy- the function it must mirror - and takes the same arguments in the same order, so the deploy/predict invariant has one documented home. Behaviour is unchanged (predicted addresses depend only on deployer, salt and init code, none of which moved);computedProxyAddressgas is identical at 35,280, and deployed bytecode grows by 7 bytes per factory (12 for the beacon one). Finding D-2 of the v0.5.0 code-quality review. _deployAndRegisterProxycachescmtatCounterIdin a local instead of loading the slot twice (once for the event, once for the increment): 114 gas per deployment, measured. Finding B-1 of the v0.5.0 code-quality review.- Marked every
internalfunctionvirtual(18 additions across 8 files), so the internal surface is now 20/20 and matches the public one, which was already 13/13. Previously the rule was applied inconsistently - insideCMTATFactoryRoot,_deployAndRegisterProxysat between twovirtualsiblings without the keyword - which left the deployment funnel and the_checkProxyAdminOwnervalidation hook impossible to override without forking the base contract. The runtime bytecode is byte-identical for all five factories (verified with the solc metadata trailer stripped), so this costs nothing to deploy or run. Finding E-1 of the v0.5.0 code-quality review; the two highest-consequence hooks are covered bytest/VirtualOverride.test.js.
Dependencies
- Updated the Hardhat Solidity compiler from
0.8.34to0.8.36(hardhat.config.js), matching the compiler the pinned CMTAT submodule builds with. Source pragmas stay^0.8.20; the EVM target staysprague. - Updated the pinned CMTAT submodule from
v3.3.0-rc1tov3.3.0-rc3. - Updated OpenZeppelin Contracts and Contracts-Upgradeable from
5.6.1to5.7.0. - Relaxed the
npm run check:ozguard (scripts/check-oz-version.js) from an exact-range match to a same-major floor: it still fails when the factory's OpenZeppelin is older than, or on a different major to, the version the pinned CMTAT submodule declares - the duplicateInitializablebreakage the guard was written for - but a newer OZ within the same major is now reported as a warning instead of an error. This is what CMTATv3.3.0-rc3needs, since it still pins OZ5.6.1exactly while the factory runs5.7.0(compiles clean, full suite passing).
Documentation
- Split the README in two: the root
README.mdis now a short overview (factory table, key features, common API, quick start, documentation index, security), and the full specification moved todoc/README.mdwith all relative links rewritten for its new location. - Completed NatSpec coverage across
contracts/: every contract, interface, library, struct, event, state variable, constant and function now carries a/** */block, with one@paramper argument and one@returnper return value. Comment-only change, verified by the style checker and by a full compile. - Fixed the stale
ContractVersionversion shown in the library-contracts table (was"0.3.0"). - Added the versioned specification PDF (
doc/specification/CMTATFactorySpecificationV0.4.0.pdf) and its cover page sources (coverpage.odg,coverpage.pdf). - Disclosed the use of AI coding assistants (Claude Code, Codex) in both READMEs.
- Added a PlantUML diagram directory (
doc/schema/plantuml/) holding both the.pumlsources and their renders. Added anOverviewdiagram (deployer -> factory -> CREATE2 proxy -> CMTAT implementation, with the factory/proxy/implementation matrix) to the top of both READMEs, and replaced the drawio export of the beacon factory withbeacon-factory.png. The redrawn beacon diagram corrects a stale label: the implementation behindCMTAT_BEACON_FACTORYisCMTATStandardUpgradeable, notCMTATUpgradeable, and it now also shows that the beacon is created once in the factory constructor. Removed the supersededdoc/schema/drawio/factory-BeaconFactory.drawio.png(thefactory.drawiosource is kept - it still backs the transparent-factory diagram). - Regenerated the Surya call graphs, inheritance graphs and markdown reports for the current contract set: 13 -> 30 of each, covering the five
Ownable2Stepdeployables, the per-family bases, the access-control modules and the extracted interfaces. Refreshed the Surya tables inlined indoc/README.md(CMTATFactoryRootstill listedAccessControlas a base and was missing three functions) and corrected example paths in the Surya script table that pointed at directories which never existed. - Fixed
doc/script/script_sol2uml.sh, which was a verbatim copy from the CMTA RuleEngine repository: its manifest listed RuleEngine contracts atsrc/andlib/CMTAT/paths absent from this project, so it failed on its first entry and the repo had never produced UML output. Rewritten around this project's 27 production contracts, writing todoc/schema/sol2uml/. It renders PNG by asking sol2uml for Graphvizdotoutput and runningdot -Tpngitself, because sol2uml's own PNG writer goes through a headless-Chromium converter that fails on current Node (Cannot read properties of undefined (reading 'html'));FORMAT=svgskips Graphviz entirely. - Added versioned Slither (0.11.5) and Aderyn (0.6.5) static-analysis reports for v0.5.0 under
doc/audits/v0.5.0/with per-finding triage feedback, and registered them inAUDIT_OVERVIEW.md. Neither tool reports anything to fix. Slither's factory-scoped checklist is empty (verified: the filter paths exist, the report cites no dependency, and all 155 unfiltered findings trace tonode_modules/orCMTAT/). Aderyn reports 1 High + 5 Low, all false-positive, by-design or environment - including the one finding new to this release,Empty Block, which flags the two_authorizeDeployCMTAToverrides whose bodies are empty because the access check rides on the modifier. Scope grew from 12 to 27 files (413 -> 697 nSLOC) with theOwnable2Stepvariants and the module split; Aderyn's centralization-risk count actually fell from 6 to 4, since the role check moved out of five entrypoints into two policy modules. - Added a code-quality review for v0.5.0 (
doc/audits/v0.5.0/CLAUDE_ANALYSIS.md), produced with Claude Code and registered inAUDIT_OVERVIEW.md. It reports no vulnerabilities. Applied from it: a measured 114-gas saving in_deployAndRegisterProxy(the deployment counter was loaded twice; the optimizer does not forward the load across the emit), and four documentation corrections - the agent-guide file tree omitted three of the sevenlibraries/files, "all three factories" and "three factory families" should read five and four,VERSIONbelongs toContractVersionrather thanCMTATFactoryRoot, and the root README API sketch declared the entrypointsexternalreturningaddresswhen they arepublicand return the concrete proxy type. Two findings are left open for a maintainer decision:virtualconsistency on internal functions, and the lack of a public getter forcustomSaltUsed.