diff --git a/AGENTS.md b/AGENTS.md
index 98d7d7d..d2a17fe 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -16,7 +16,7 @@ This file helps AI agents understand and work with this codebase.
The factories deploy token proxies with `CREATE2`, track deployed instances by incremental id, and gate deployment behind `AccessControl`.
-- **Factory version:** `0.3.0` in `contracts/libraries/ContractVersion.sol`
+- **Factory version:** `0.4.0` in `contracts/libraries/ContractVersion.sol`
- **Solidity:** source files use `^0.8.20`, Hardhat compiles with `0.8.34`
- **EVM target:** `prague`
- **License:** `MPL-2.0`
@@ -70,7 +70,7 @@ CMTATFactoryInvariant
CMTATFactoryRoot
|- AccessControl
-|- VERSION = "0.3.0"
+|- VERSION = "0.4.0"
|- cmtatsList / cmtatCounterId / CMTATProxyAddress(id)
|- useCustomSalt
|- _checkAndDetermineDeploymentSalt(...)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0569ed9..2fd0c2b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,11 +48,60 @@ Custom changelog tag: `Dependencies`, `Documentation`, `Testing`
-## 0.3.0 - 2026/06/30
+## 0.4.0 - 2026/07/03
-Branch: `dev`
Commit: _pending release commit_
+> The fix commits for the Nethermind AuditAgent findings (NM-1, NM-2) are slated for this release. The version
+> constant is bumped now; the hardening commits land under this entry as they are made.
+
+### Security
+
+- Reviewed the Nethermind **AuditAgent** v0.3.0 report and added per-finding triage. **NM-1** (Low): the
+ counter-derived next-address helpers (`computedNextProxyAddress` / `nextDeploymentSalt`) are front-runnable
+ across authorized deployers, because in counter mode the effective CREATE2 salt is the shared
+ `keccak256(cmtatCounterId)`. **NM-2** (Info): that same counter-derived salt can be reused under reentrant proxy
+ initialization, since `Create2.deploy` runs before `++cmtatCounterId`. See
+ [`doc/audits/v0.3.0/audit_agent_report-feedback.md`](doc/audits/v0.3.0/audit_agent_report-feedback.md).
+- **NM-1 documentation clarification.** Added a `WARNING` NatSpec block to `nextDeploymentSalt()` and to
+ `computedNextProxyAddress(...)` on all five factories, plus a warning callout in the README "Salt behavior"
+ section, stating that in counter mode (`useCustomSalt == false`) a predicted address is only valid until the next
+ deployment by any authorized deployer, and that the safer mode is custom salts (`useCustomSalt == true`) with a
+ unique, caller-chosen, one-time-use salt. Documentation-only; no behavior change.
+- **NM-2 reentrancy guard.** Each concrete factory inherits OpenZeppelin `ReentrancyGuard` (co-located with usage)
+ and marks its public `deployCMTAT(...)` entrypoint `nonReentrant`. This prevents a
+ reentrant `deployCMTAT(...)` — triggered during a proxy's constructor/initializer, before `++cmtatCounterId` — from
+ observing the same `cmtatCounterId` and reusing the auto-derived salt, so every automatic deployment keeps a
+ distinct counter and salt. The guard is declared first in the modifier list (`nonReentrant onlyRole(...)`) per
+ the Aderyn best-practice check.
+
+### Changed
+
+- Bumped the factory version constant to `0.4.0` (`ContractVersion.sol`) and synced every mirror (`package.json`,
+ the `version()` test, README, `AGENTS.md` / `CLAUDE.md`).
+
+### Documentation
+
+- Added the Nethermind AuditAgent v0.3.0 report (`doc/audits/v0.3.0/audit_agent_report_v0.3.0.pdf`) and its
+ per-finding triage feedback, and recorded both in [`doc/audits/AUDIT_OVERVIEW.md`](doc/audits/AUDIT_OVERVIEW.md)
+ (neither finding exploitable; both hardened in this release — NM-1 docs warning, NM-2 reentrancy guard).
+- Added `doc/script/convert_links_for_pdf.sh`, a helper that rewrites relative Markdown links to GitHub URLs for
+ PDF generation while preserving image and external links.
+- Added versioned Slither (0.11.5) and Aderyn (0.6.5) static-analysis reports for v0.4.0 under `doc/audits/v0.4.0/`
+ with per-finding triage feedback (both tools: nothing to fix). The Aderyn set matches v0.3.0 (1 High false
+ positive + 4 by-design/environment Lows); Slither's factory-scoped result stays 0.
+
+### Testing
+
+- Added `test/UUPS/ReentrancyGuard.test.js` and `contracts/mocks/ReentrancyDeployMock.sol`: a malicious `logic`
+ mock re-enters `deployCMTAT` from the proxy initializer through a role-holding attacker. The armed case reverts
+ and registers nothing (the `nonReentrant` guard fires); a disarmed control deployment succeeds — isolating the
+ guard as the cause (NM-2). Suite: 42 passing.
+
+## 0.3.0 - 2026/06/30
+
+Commit: `18a8e66c70c810647694e5dc436e895a07016ec8`
+
### Added
- CMTAT Light factories `CMTAT_LIGHT_TP_FACTORY` and `CMTAT_LIGHT_BEACON_FACTORY` (`contracts/light/`) deploying the lighter `CMTATUpgradeableLight` implementation through the smaller `CMTAT_LIGHT_ARGUMENT` initializer struct.
diff --git a/CLAUDE.md b/CLAUDE.md
index 98d7d7d..d2a17fe 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -16,7 +16,7 @@ This file helps AI agents understand and work with this codebase.
The factories deploy token proxies with `CREATE2`, track deployed instances by incremental id, and gate deployment behind `AccessControl`.
-- **Factory version:** `0.3.0` in `contracts/libraries/ContractVersion.sol`
+- **Factory version:** `0.4.0` in `contracts/libraries/ContractVersion.sol`
- **Solidity:** source files use `^0.8.20`, Hardhat compiles with `0.8.34`
- **EVM target:** `prague`
- **License:** `MPL-2.0`
@@ -70,7 +70,7 @@ CMTATFactoryInvariant
CMTATFactoryRoot
|- AccessControl
-|- VERSION = "0.3.0"
+|- VERSION = "0.4.0"
|- cmtatsList / cmtatCounterId / CMTATProxyAddress(id)
|- useCustomSalt
|- _checkAndDetermineDeploymentSalt(...)
diff --git a/README.md b/README.md
index 70cd12c..78ea119 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,5 @@
# CMTAT Factory
-> This project is not audited
->
-> If you want to use this project, perform your own verification or send an email to [admin@cmta.ch](mailto:admin@cmta.ch).
-
## Introduction
This project provides a modular deployment framework for [**CMTAT**](https://github.com/CMTA/CMTAT), a security token framework, using three upgradeability patterns: **UUPS proxy**, **Transparent proxy**, and **Beacon proxy**.
@@ -11,6 +7,8 @@ Each factory contract automates deployment using **deterministic addresses (via
In addition to the three standard factories, two **CMTAT Light** factories (`CMTAT_LIGHT_TP_FACTORY` and `CMTAT_LIGHT_BEACON_FACTORY`) deploy the lighter `CMTATUpgradeableLight` implementation. They behave exactly like their standard Transparent/Beacon counterparts but take the smaller `CMTAT_LIGHT_ARGUMENT` initializer struct (admin + ERC20 attributes only).
+> **Note:** This project has not undergone an audit and is provided as-is without any warranties.
+
## Table of Contents
- [Introduction](#introduction)
@@ -156,7 +154,7 @@ Every factory deploys its proxies with the **`CREATE2`** opcode instead of the d
address = keccak256(0xff ++ deployerAddress ++ salt ++ keccak256(init_code))[12:]
```
- The address depends only on the **deployer address**, a caller-chosen **`salt`**, and the **`init_code`** (creation bytecode + ABI-encoded constructor arguments) — **not** on the nonce. Given those three inputs the address is fully deterministic and can be computed *before* deploying (a "counterfactual" address).
+ The address depends only on the **deployer address** — here the **factory contract address**, since the proxy is deployed through `Create2.deploy` executed inside the factory — a caller-chosen **`salt`**, and the **`init_code`** (creation bytecode + ABI-encoded constructor arguments) — **not** on the nonce. Given those three inputs the address is fully deterministic and can be computed *before* deploying (a "counterfactual" address).
**Why this project uses `CREATE2`**
@@ -173,7 +171,7 @@ All factories inherit `CMTATFactoryRoot` and share the following surface, in add
Factories implement [ERC-8303](https://github.com/ethereum/ERCs/pull/1819) (still a draft pull request, not yet published on the Ethereum website) through `ContractVersion` and expose the factory version:
```solidity
-function version() external view returns (string memory); // current: "0.3.0"
+function version() external view returns (string memory); // current: "0.4.0"
```
`supportsInterface(bytes4)` (ERC-165) returns `true` for `type(IERC8303).interfaceId`, in addition to the `AccessControl` interfaces.
@@ -208,10 +206,29 @@ It records the deployed `proxy`, the `deployer` (`msg.sender`), the incremental
- When `useCustomSalt == true`, the caller-supplied salt is used directly and is **one-time-use**: reusing a salt reverts with `FactoryErrors.CMTAT_Factory_SaltAlreadyUsed()`.
- `computedProxyAddress(...)` takes an *effective* salt and mirrors the exact bytecode used by `deployCMTAT(...)`, while `computedNextProxyAddress(...)` applies the same salt-selection logic as a real deployment.
+> **⚠️ Warning — counter mode has no per-caller address reservation.**
+> When `useCustomSalt == false`, the salt is the shared `keccak256(abi.encodePacked(cmtatCounterId))`, so
+> `nextDeploymentSalt()` and `computedNextProxyAddress(...)` only predict the address of the *factory's* next
+> deployment — by whichever authorized deployer transacts first. If another `CMTAT_DEPLOYER_ROLE` holder deploys
+> before you, the counter advances and your token lands at a **different** address; anything you pre-funded or
+> pre-authorized at the predicted address is stranded.
+>
+> **The safer mode is to enable custom salts (`useCustomSalt == true`) and pass a unique, caller-chosen `salt`.**
+> A custom salt is one-time-use (reuse reverts with `CMTAT_Factory_SaltAlreadyUsed`), binds the predicted address
+> to *your* deployment, and cannot be shifted by another deployer — so `computedProxyAddress(...)` /
+> `computedNextProxyAddress(...)` give you a stable, reservable address you can safely pre-fund. Use counter mode
+> only for single-deployer or coordinated deployments.
+
### Access control
All factories inherit OpenZeppelin `AccessControl`. The constructor grants both `DEFAULT_ADMIN_ROLE` and `CMTAT_DEPLOYER_ROLE` to `factoryAdmin`, and `deployCMTAT(...)` is gated by `onlyRole(CMTAT_DEPLOYER_ROLE)`.
+### Reentrancy protection
+
+Each concrete factory inherits OpenZeppelin `ReentrancyGuard` and marks its `deployCMTAT(...)` entrypoint `nonReentrant` (declared first, before `onlyRole`).
+
+This matters because in counter mode the effective salt is derived from `cmtatCounterId`, which is only incremented **after** `Create2.deploy`. Since `Create2.deploy` runs the proxy constructor — and its CMTAT initializer — before that increment, a reentrant `deployCMTAT(...)` call could otherwise observe the same `cmtatCounterId` and reuse the same auto-derived salt (Nethermind AuditAgent NM-2). The guard rejects any attempt to re-enter the deploy path mid-deployment, so every automatic deployment sees a distinct counter value and salt, keeping the deployment index and emitted events consistent.
+
## Factory contracts
### Proxy patterns: Transparent vs UUPS vs Beacon
@@ -677,11 +694,11 @@ Please see [SECURITY.md](https://github.com/CMTA/CMTAT/blob/master/SECURITY.md)
### Audit
-This project is not audited !
+> **Note:** This project has not undergone an audit and is provided as-is without any warranties.
### Tools
-Static-analysis reports are versioned under [`doc/audits/`](./doc/audits/); see the [audit overview](./doc/audits/AUDIT_OVERVIEW.md) for the consolidated results and triage. All runs exclude mocks and exclude dependencies / the CMTAT submodule from scope. **For v0.3.0, neither tool reports anything to fix.**
+Static-analysis reports are versioned under [`doc/audits/`](./doc/audits/); see the [audit overview](./doc/audits/AUDIT_OVERVIEW.md) for the consolidated results and triage. All runs exclude mocks and exclude dependencies / the CMTAT submodule from scope. **For v0.4.0, neither tool reports anything to fix.**
#### [Slither](https://github.com/crytic/slither)
@@ -693,6 +710,7 @@ slither . --checklist --filter-paths "node_modules,CMTAT,test,forge-std,mocks" >
| Version | Report | Feedback |
| --- | --- | --- |
+| v0.4.0 | [slither-report.md](./doc/audits/v0.4.0/slither-report.md) | [feedback](./doc/audits/v0.4.0/slither-report-feedback.md) |
| v0.3.0 | [slither-report.md](./doc/audits/v0.3.0/slither-report.md) | [feedback](./doc/audits/v0.3.0/slither-report-feedback.md) |
| v0.2.0 | [slither-report.md](./doc/audits/v0.2.0/slither-report.md) | — |
@@ -706,17 +724,28 @@ aderyn -x mocks --output aderyn-report.md
| Version | Report | Feedback |
| --- | --- | --- |
+| v0.4.0 | [aderyn-report.md](./doc/audits/v0.4.0/aderyn-report.md) | [feedback](./doc/audits/v0.4.0/aderyn-report-feedback.md) |
| v0.3.0 | [aderyn-report.md](./doc/audits/v0.3.0/aderyn-report.md) | [feedback](./doc/audits/v0.3.0/aderyn-report-feedback.md) |
| v0.2.0 | [aderyn-report.md](./doc/audits/v0.2.0/aderyn-report.md) | — |
+#### [Nethermind AuditAgent](https://auditagent.nethermind.io/)
+
+[Nethermind AuditAgent](https://auditagent.nethermind.io/) is an AI-powered automated smart-contract scanner. The report was independently verified against the source in the linked feedback file.
+
+> Note: This scan was performed by an AI-powered automated tool, not a formal human-led audit.
+
+| Version | Result | Report | Feedback |
+| --- | --- | --- | --- |
+| v0.3.0 | 0 High · 0 Medium · 1 Low · 1 Info — **nothing to fix** (both accepted as design; hardened in v0.4.0) | [audit_agent_report_v0.3.0.pdf](./doc/audits/v0.3.0/audit_agent_report_v0.3.0.pdf) | [feedback](./doc/audits/v0.3.0/audit_agent_report-feedback.md) |
+
## Further reading
For more details and test scenario, you can read this article on the Taurus blog: [Making CMTAT Tokenization More Scalable and Cost-Effective with Proxy and Factory Contracts](https://www.taurushq.com/blog/cmtat-tokenization-deployment-with-proxy-and-factory/).
-This article uses the CMTAT version [2.5.1](https://github.com/CMTA/CMTAT/releases/tag/v2.5.1) when the factory code was still included in the CMTAT repository, corresponding to Factory release `0.1.0`. The current factory version is `0.3.0` (exposed on-chain through `version()`, see [Versioning (ERC-8303)](#versioning-erc-8303)).
+This article uses the CMTAT version [2.5.1](https://github.com/CMTA/CMTAT/releases/tag/v2.5.1) when the factory code was still included in the CMTAT repository, corresponding to Factory release `0.1.0`. The current factory version is `0.4.0` (exposed on-chain through `version()`, see [Versioning (ERC-8303)](#versioning-erc-8303)).
## Intellectual property
-The code is copyright (c) Capital Market and Technology Association, 2018-2025, and is released under [Mozilla Public License 2.0](./LICENSE.md).
+The code is copyright (c) Capital Market and Technology Association, 2025-2026, and is released under [Mozilla Public License 2.0](./LICENSE.md).
diff --git a/contracts/libraries/CMTATFactoryRoot.sol b/contracts/libraries/CMTATFactoryRoot.sol
index a7c9198..313969e 100644
--- a/contracts/libraries/CMTATFactoryRoot.sol
+++ b/contracts/libraries/CMTATFactoryRoot.sol
@@ -9,7 +9,7 @@ import {CMTATFactoryInvariant} from "./CMTATFactoryInvariant.sol";
import {FactoryErrors} from "./FactoryErrors.sol";
/**
* @notice Code common to Beacon, TP and UUPS factory
-*
+*
*/
abstract contract CMTATFactoryRoot is AccessControl, ContractVersion, CMTATFactoryInvariant {
/* ============ State Variables ============ */
@@ -68,6 +68,10 @@ abstract contract CMTATFactoryRoot is AccessControl, ContractVersion, CMTATFacto
/**
* @notice Returns the effective salt that will be used for the next deployment when custom salts are disabled.
* @dev Custom-salt deployments use the caller-provided salt directly.
+ * @dev WARNING: this salt is derived from the shared `cmtatCounterId`, so it advances whenever ANY authorized
+ * deployer deploys. An address predicted from it is not reserved for a specific caller and can be front-run by
+ * another `CMTAT_DEPLOYER_ROLE` holder. For a stable, reservable address prefer custom-salt mode
+ * (`useCustomSalt == true`) with a unique caller-chosen salt, which is enforced one-time-use.
*/
function nextDeploymentSalt() public view virtual returns(bytes32 saltBytes) {
return keccak256(abi.encodePacked(cmtatCounterId));
@@ -104,6 +108,11 @@ abstract contract CMTATFactoryRoot is AccessControl, ContractVersion, CMTATFacto
/**
* @dev Deploy CMTAT proxy and register it in the factory index.
+ * @dev Reentrancy window: Create2.deploy below runs the proxy constructor (and its CMTAT initializer) BEFORE
+ * cmtatCounterId is incremented, so a re-entry into the deploy path could observe the same counter and reuse the
+ * same counter-derived salt (Nethermind AuditAgent NM-2). Each concrete factory inherits OpenZeppelin
+ * ReentrancyGuard and marks its public deployCMTAT entrypoint `nonReentrant`, which blocks that re-entry.
+ * All five factories funnel through this function.
*/
function _deployAndRegisterProxy(bytes memory bytecode, bytes32 deploymentSalt) internal returns (address cmtatAddress) {
cmtatAddress = Create2.deploy(0, deploymentSalt, bytecode);
diff --git a/contracts/libraries/ContractVersion.sol b/contracts/libraries/ContractVersion.sol
index c66e876..fabd1ab 100644
--- a/contracts/libraries/ContractVersion.sol
+++ b/contracts/libraries/ContractVersion.sol
@@ -19,7 +19,7 @@ abstract contract ContractVersion is ERC165, IERC8303 {
/**
* @notice Get the current version of the smart contract.
*/
- string private constant VERSION = "0.3.0";
+ string private constant VERSION = "0.4.0";
/*//////////////////////////////////////////////////////////////
PUBLIC/EXTERNAL FUNCTIONS
diff --git a/contracts/light/CMTAT_LIGHT_BEACON_FACTORY.sol b/contracts/light/CMTAT_LIGHT_BEACON_FACTORY.sol
index 6536f6a..ba6db7c 100644
--- a/contracts/light/CMTAT_LIGHT_BEACON_FACTORY.sol
+++ b/contracts/light/CMTAT_LIGHT_BEACON_FACTORY.sol
@@ -3,13 +3,14 @@ pragma solidity ^0.8.20;
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import {CMTATUpgradeableLight} from "../../CMTAT/contracts/deployment/light/CMTATUpgradeableLight.sol";
+import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {CMTATBeaconFactoryBase} from "../libraries/CMTATBeaconFactoryBase.sol";
/**
* @notice Factory to deploy CMTAT Light with a beacon proxy
*
*/
-contract CMTAT_LIGHT_BEACON_FACTORY is CMTATBeaconFactoryBase {
+contract CMTAT_LIGHT_BEACON_FACTORY is CMTATBeaconFactoryBase, ReentrancyGuard {
/**
* @notice Deploys a factory that manages CMTAT Light Beacon proxies.
*
@@ -40,7 +41,7 @@ contract CMTAT_LIGHT_BEACON_FACTORY is CMTATBeaconFactoryBase {
function deployCMTAT(
bytes32 deploymentSaltInput,
CMTAT_LIGHT_ARGUMENT calldata cmtatArgument
- ) public virtual onlyRole(CMTAT_DEPLOYER_ROLE) returns(BeaconProxy cmtat) {
+ ) public virtual nonReentrant onlyRole(CMTAT_DEPLOYER_ROLE) returns(BeaconProxy cmtat) {
return _deployBeaconProxy(deploymentSaltInput, _initializerData(cmtatArgument));
}
@@ -58,6 +59,10 @@ contract CMTAT_LIGHT_BEACON_FACTORY is CMTATBeaconFactoryBase {
/**
* @notice get the proxy address using the same salt selection as deployCMTAT
+ * @dev WARNING: in counter mode (`useCustomSalt == false`) this prediction is only valid until the next
+ * deployment by ANY authorized deployer, because the salt is the shared `nextDeploymentSalt()`. Do not pre-fund
+ * or pre-authorize the returned address in a multi-deployer setup. For a stable, reservable address use
+ * custom-salt mode (`useCustomSalt == true`) with a unique caller-chosen salt (one-time-use).
*/
function computedNextProxyAddress(
bytes32 deploymentSaltInput,
diff --git a/contracts/light/CMTAT_LIGHT_TP_FACTORY.sol b/contracts/light/CMTAT_LIGHT_TP_FACTORY.sol
index 36c9257..cc0f9a1 100644
--- a/contracts/light/CMTAT_LIGHT_TP_FACTORY.sol
+++ b/contracts/light/CMTAT_LIGHT_TP_FACTORY.sol
@@ -3,13 +3,14 @@ pragma solidity ^0.8.20;
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {CMTATUpgradeableLight} from "../../CMTAT/contracts/deployment/light/CMTATUpgradeableLight.sol";
+import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {CMTATTransparentFactoryBase} from "../libraries/CMTATTransparentFactoryBase.sol";
/**
* @notice Factory to deploy CMTAT Light with a transparent proxy
*
*/
-contract CMTAT_LIGHT_TP_FACTORY is CMTATTransparentFactoryBase {
+contract CMTAT_LIGHT_TP_FACTORY is CMTATTransparentFactoryBase, ReentrancyGuard {
/**
* @param logic_ contract implementation, cannot be zero
* @param factoryAdmin admin
@@ -33,7 +34,7 @@ contract CMTAT_LIGHT_TP_FACTORY is CMTATTransparentFactoryBase {
bytes32 deploymentSaltInput,
address proxyAdminOwner,
CMTAT_LIGHT_ARGUMENT calldata cmtatArgument
- ) public virtual onlyRole(CMTAT_DEPLOYER_ROLE) returns(TransparentUpgradeableProxy cmtat) {
+ ) public virtual nonReentrant onlyRole(CMTAT_DEPLOYER_ROLE) returns(TransparentUpgradeableProxy cmtat) {
return _deployTransparentProxy(deploymentSaltInput, proxyAdminOwner, _initializerData(cmtatArgument));
}
@@ -52,6 +53,10 @@ contract CMTAT_LIGHT_TP_FACTORY is CMTATTransparentFactoryBase {
/**
* @notice get the proxy address using the same salt selection as deployCMTAT
+ * @dev WARNING: in counter mode (`useCustomSalt == false`) this prediction is only valid until the next
+ * deployment by ANY authorized deployer, because the salt is the shared `nextDeploymentSalt()`. Do not pre-fund
+ * or pre-authorize the returned address in a multi-deployer setup. For a stable, reservable address use
+ * custom-salt mode (`useCustomSalt == true`) with a unique caller-chosen salt (one-time-use).
*/
function computedNextProxyAddress(
bytes32 deploymentSaltInput,
diff --git a/contracts/mocks/ReentrancyDeployMock.sol b/contracts/mocks/ReentrancyDeployMock.sol
new file mode 100644
index 0000000..e317919
--- /dev/null
+++ b/contracts/mocks/ReentrancyDeployMock.sol
@@ -0,0 +1,63 @@
+//SPDX-License-Identifier: MPL-2.0
+pragma solidity ^0.8.20;
+
+/**
+* @title Reentrancy test doubles for the CMTAT factories
+* @notice Test-only contracts that prove the `nonReentrant` guard on the factories' public
+* `deployCMTAT(...)` entrypoints blocks a `deployCMTAT` re-entry triggered from a proxy's
+* initializer while the outer deployment is still in flight (Nethermind AuditAgent NM-2).
+* @dev Excluded from static analysis (the `mocks` folder is filtered by Slither/Aderyn).
+*/
+
+/**
+* @notice Holds `CMTAT_DEPLOYER_ROLE` and re-enters the factory when armed.
+* @dev The re-entry must come from a role holder: routing it through this contract (instead of the
+* delegatecall'd logic, whose `msg.sender` would be the half-constructed proxy) ensures the nested
+* `deployCMTAT` passes `onlyRole` and is rejected by the reentrancy guard, not by access control.
+*/
+contract ReentrancyDeployAttacker {
+ address public factory;
+ bytes public reentrantCall;
+ bool public armed;
+
+ function configure(address factory_, bytes calldata reentrantCall_, bool armed_) external {
+ factory = factory_;
+ reentrantCall = reentrantCall_;
+ armed = armed_;
+ }
+
+ /// @dev Invoked (as a normal call) from the proxy initializer during the outer deployment.
+ function attack() external {
+ if (!armed) {
+ return;
+ }
+ (bool ok, bytes memory ret) = factory.call(reentrantCall);
+ if (!ok) {
+ // Bubble the inner revert (ReentrancyGuardReentrantCall) up into the constructor.
+ assembly {
+ revert(add(ret, 0x20), mload(ret))
+ }
+ }
+ }
+}
+
+/**
+* @notice Stands in for the CMTAT implementation ("logic") passed to a factory.
+* @dev The factory delegatecalls the CMTAT initialize calldata into this contract during proxy
+* construction; there is no matching function, so execution lands in `fallback` and re-enters the
+* factory through the attacker. The attacker address is `immutable`, so it is baked into the runtime
+* bytecode and remains readable under the proxy's delegatecall.
+*/
+contract ReentrantInitLogicMock {
+ address private immutable ATTACKER;
+
+ constructor(address attacker_) {
+ ATTACKER = attacker_;
+ }
+
+ fallback() external payable {
+ ReentrancyDeployAttacker(ATTACKER).attack();
+ }
+
+ receive() external payable {}
+}
diff --git a/contracts/standard/CMTAT_BEACON_FACTORY.sol b/contracts/standard/CMTAT_BEACON_FACTORY.sol
index ee0b013..5ad5aa7 100644
--- a/contracts/standard/CMTAT_BEACON_FACTORY.sol
+++ b/contracts/standard/CMTAT_BEACON_FACTORY.sol
@@ -3,13 +3,14 @@ pragma solidity ^0.8.20;
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import {CMTATStandardUpgradeable} from "../../CMTAT/contracts/deployment/CMTATStandardUpgradeable.sol";
+import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {CMTATBeaconFactoryBase} from "../libraries/CMTATBeaconFactoryBase.sol";
/**
* @notice Factory to deploy beacon proxy
*
*/
-contract CMTAT_BEACON_FACTORY is CMTATBeaconFactoryBase {
+contract CMTAT_BEACON_FACTORY is CMTATBeaconFactoryBase, ReentrancyGuard {
/**
* @notice Deploys a factory that manages CMTAT Beacon proxies.
* @dev
@@ -51,7 +52,7 @@ contract CMTAT_BEACON_FACTORY is CMTATBeaconFactoryBase {
bytes32 deploymentSaltInput,
// CMTAT function initialize
CMTAT_ARGUMENT calldata cmtatArgument
- ) public virtual onlyRole(CMTAT_DEPLOYER_ROLE) returns(BeaconProxy cmtat) {
+ ) public virtual nonReentrant onlyRole(CMTAT_DEPLOYER_ROLE) returns(BeaconProxy cmtat) {
return _deployBeaconProxy(deploymentSaltInput, _initializerData(cmtatArgument));
}
@@ -70,6 +71,10 @@ contract CMTAT_BEACON_FACTORY is CMTATBeaconFactoryBase {
/**
* @notice get the proxy address using the same salt selection as deployCMTAT
+ * @dev WARNING: in counter mode (`useCustomSalt == false`) this prediction is only valid until the next
+ * deployment by ANY authorized deployer, because the salt is the shared `nextDeploymentSalt()`. Do not pre-fund
+ * or pre-authorize the returned address in a multi-deployer setup. For a stable, reservable address use
+ * custom-salt mode (`useCustomSalt == true`) with a unique caller-chosen salt (one-time-use).
*/
function computedNextProxyAddress(
bytes32 deploymentSaltInput,
diff --git a/contracts/standard/CMTAT_TP_FACTORY.sol b/contracts/standard/CMTAT_TP_FACTORY.sol
index ce0ba35..39aa74b 100644
--- a/contracts/standard/CMTAT_TP_FACTORY.sol
+++ b/contracts/standard/CMTAT_TP_FACTORY.sol
@@ -3,6 +3,7 @@ pragma solidity ^0.8.20;
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {CMTATStandardUpgradeable} from "../../CMTAT/contracts/deployment/CMTATStandardUpgradeable.sol";
+import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {CMTATTransparentFactoryBase} from "../libraries/CMTATTransparentFactoryBase.sol";
@@ -10,7 +11,7 @@ import {CMTATTransparentFactoryBase} from "../libraries/CMTATTransparentFactoryB
* @notice Factory to deploy CMTAT with a transparent proxy
*
*/
-contract CMTAT_TP_FACTORY is CMTATTransparentFactoryBase {
+contract CMTAT_TP_FACTORY is CMTATTransparentFactoryBase, ReentrancyGuard {
/**
* @param logic_ contract implementation, cannot be zero
@@ -42,7 +43,7 @@ contract CMTAT_TP_FACTORY is CMTATTransparentFactoryBase {
address proxyAdminOwner,
// CMTAT function initialize
CMTAT_ARGUMENT calldata cmtatArgument
- ) public virtual onlyRole(CMTAT_DEPLOYER_ROLE) returns(TransparentUpgradeableProxy cmtat) {
+ ) public virtual nonReentrant onlyRole(CMTAT_DEPLOYER_ROLE) returns(TransparentUpgradeableProxy cmtat) {
return _deployTransparentProxy(deploymentSaltInput, proxyAdminOwner, _initializerData(cmtatArgument));
}
@@ -63,6 +64,10 @@ contract CMTAT_TP_FACTORY is CMTATTransparentFactoryBase {
/**
* @notice get the proxy address using the same salt selection as deployCMTAT
+ * @dev WARNING: in counter mode (`useCustomSalt == false`) this prediction is only valid until the next
+ * deployment by ANY authorized deployer, because the salt is the shared `nextDeploymentSalt()`. Do not pre-fund
+ * or pre-authorize the returned address in a multi-deployer setup. For a stable, reservable address use
+ * custom-salt mode (`useCustomSalt == true`) with a unique caller-chosen salt (one-time-use).
*/
function computedNextProxyAddress(
bytes32 deploymentSaltInput,
diff --git a/contracts/standard/CMTAT_UUPS_FACTORY.sol b/contracts/standard/CMTAT_UUPS_FACTORY.sol
index fc6bec8..59fd9ed 100644
--- a/contracts/standard/CMTAT_UUPS_FACTORY.sol
+++ b/contracts/standard/CMTAT_UUPS_FACTORY.sol
@@ -4,6 +4,7 @@ pragma solidity ^0.8.20;
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {CMTATUpgradeableUUPS} from "../../CMTAT/contracts/deployment/CMTATUpgradeableUUPS.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
+import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {CMTATFactoryBase} from "../libraries/CMTATFactoryBase.sol";
@@ -11,7 +12,7 @@ import {CMTATFactoryBase} from "../libraries/CMTATFactoryBase.sol";
* @notice Factory to deploy CMTAT with a UUPS proxy
*
*/
-contract CMTAT_UUPS_FACTORY is CMTATFactoryBase {
+contract CMTAT_UUPS_FACTORY is CMTATFactoryBase, ReentrancyGuard {
/**
* @param logic_ contract implementation, cannot be zero
* @param factoryAdmin admin
@@ -40,7 +41,7 @@ contract CMTAT_UUPS_FACTORY is CMTATFactoryBase {
bytes32 deploymentSaltInput,
// CMTAT function initialize
CMTAT_ARGUMENT calldata cmtatArgument
- ) public virtual onlyRole(CMTAT_DEPLOYER_ROLE) returns(ERC1967Proxy cmtat) {
+ ) public virtual nonReentrant onlyRole(CMTAT_DEPLOYER_ROLE) returns(ERC1967Proxy cmtat) {
bytes32 deploymentSalt = _checkAndDetermineDeploymentSalt(deploymentSaltInput);
bytes memory bytecode = _getBytecode(
// CMTAT function initialize
@@ -68,6 +69,10 @@ contract CMTAT_UUPS_FACTORY is CMTATFactoryBase {
/**
* @notice get the proxy address using the same salt selection as deployCMTAT
+ * @dev WARNING: in counter mode (`useCustomSalt == false`) this prediction is only valid until the next
+ * deployment by ANY authorized deployer, because the salt is the shared `nextDeploymentSalt()`. Do not pre-fund
+ * or pre-authorize the returned address in a multi-deployer setup. For a stable, reservable address use
+ * custom-salt mode (`useCustomSalt == true`) with a unique caller-chosen salt (one-time-use).
*/
function computedNextProxyAddress(
bytes32 deploymentSaltInput,
diff --git a/doc/audits/AUDIT_OVERVIEW.md b/doc/audits/AUDIT_OVERVIEW.md
index 2d15e64..d708f3e 100644
--- a/doc/audits/AUDIT_OVERVIEW.md
+++ b/doc/audits/AUDIT_OVERVIEW.md
@@ -14,19 +14,41 @@ In scope: the factory contracts under `contracts/` (`standard/`, `light/`, `libr
| Version | Tool | Report | Feedback (triage) |
| --- | --- | --- | --- |
+| v0.4.0 | Slither 0.11.5 | [slither-report.md](./v0.4.0/slither-report.md) | [feedback](./v0.4.0/slither-report-feedback.md) |
+| v0.4.0 | Aderyn 0.6.5 | [aderyn-report.md](./v0.4.0/aderyn-report.md) | [feedback](./v0.4.0/aderyn-report-feedback.md) |
| v0.3.0 | Slither 0.11.5 | [slither-report.md](./v0.3.0/slither-report.md) | [feedback](./v0.3.0/slither-report-feedback.md) |
| v0.3.0 | Aderyn 0.6.5 | [aderyn-report.md](./v0.3.0/aderyn-report.md) | [feedback](./v0.3.0/aderyn-report-feedback.md) |
+| v0.3.0 | [Nethermind AuditAgent](https://auditagent.nethermind.io/) (AI) | [audit_agent_report_v0.3.0.pdf](./v0.3.0/audit_agent_report_v0.3.0.pdf) | [feedback](./v0.3.0/audit_agent_report-feedback.md) |
| v0.2.0 | Slither | [slither-report.md](./v0.2.0/slither-report.md) | — |
| v0.2.0 | Aderyn | [aderyn-report.md](./v0.2.0/aderyn-report.md) | — |
All runs **exclude mocks** and exclude dependencies / the CMTAT submodule from scope.
+> **Note:** the [Nethermind AuditAgent](https://auditagent.nethermind.io/) scan was performed by an AI-powered
+> automated tool, not a formal human-led audit. Its findings are AI-generated leads, independently verified against
+> the source in the linked feedback file.
+
+## v0.4.0 results
+
+| Tool | High | Medium | Low | Info | Anything to fix? |
+| --- | --- | --- | --- | --- | --- |
+| Slither | 0 | 0 | 0 | 0 | **No** — filtered checklist empty; unfiltered detectors resolve to `node_modules` / `CMTAT/` or the excluded `contracts/mocks/` test doubles. |
+| Aderyn | 1 | 0 | 4 | 0 | **No** — H-1 is a false positive (canonical CREATE2 init-code); the four Lows are by-design / environment / benign OZ pattern. (The NM-2 guard transiently added a `nonReentrant`-not-first-modifier Low; it was fixed by reordering `deployCMTAT`'s modifiers to `nonReentrant onlyRole(...)`.) |
+
+**Conclusion for v0.4.0: nothing to fix.** The NM-2 guard's transient `nonReentrant`-not-first-modifier note was resolved by reordering the `deployCMTAT` modifiers; the final Aderyn set matches v0.3.0 (1 High false positive + 4 by-design/environment Lows). See each feedback file.
+
+## Substantive findings addressed (internal review + AuditAgent, v0.4.0)
+
+- **NM-1 (AuditAgent, Low) — counter-mode address prediction is front-runnable.** Accepted as design (custom-salt mode already reserves a per-caller address); clarified with a `WARNING` NatSpec block on `nextDeploymentSalt()` / `computedNextProxyAddress(...)` and a README callout steering integrators to custom salts.
+- **NM-2 (AuditAgent, Info) — counter-derived salt reuse under reentrant init.** Hardened by having each concrete factory inherit OZ `ReentrancyGuard` and mark its `deployCMTAT(...)` entrypoint `nonReentrant`; covered by a regression test (`test/UUPS/ReentrancyGuard.test.js` + `contracts/mocks/ReentrancyDeployMock.sol`).
+
## v0.3.0 results
| Tool | High | Medium | Low | Info | Anything to fix? |
| --- | --- | --- | --- | --- | --- |
| Slither | 0 | 0 | 0 | 0 | **No** — filtered checklist empty; all unfiltered detectors resolve to `node_modules` / `CMTAT/`. |
| Aderyn | 1 | 0 | 4 | 0 | **No** — H-1 is a false positive (canonical CREATE2 init-code); the 4 Lows are by-design / environment / benign OZ pattern. |
+| AuditAgent (AI) | 0 | 0 | 1 | 1 | **No** (not exploitable) — both are the deliberate shared counter-derived salt design; hardened in v0.4.0 anyway (NM-1 docs warning steering to custom-salt mode; NM-2 `nonReentrant` on the `deployCMTAT(...)` entrypoints). |
**Conclusion for v0.3.0: nothing to fix.** See each feedback file for the per-finding reasoning, verified against the source.
@@ -47,6 +69,6 @@ These came from internal review (not the static analyzers) and were fixed in thi
```bash
# mocks excluded (default)
slither . --checklist --filter-paths "node_modules,CMTAT,test,forge-std,mocks" \
- > doc/audits/v0.3.0/slither-report.md
-aderyn -x mocks --output doc/audits/v0.3.0/aderyn-report.md
+ > doc/audits/v0.4.0/slither-report.md
+aderyn -x mocks --output doc/audits/v0.4.0/aderyn-report.md
```
diff --git a/doc/audits/v0.3.0/audit_agent_report-feedback.md b/doc/audits/v0.3.0/audit_agent_report-feedback.md
new file mode 100644
index 0000000..4f6ce93
--- /dev/null
+++ b/doc/audits/v0.3.0/audit_agent_report-feedback.md
@@ -0,0 +1,119 @@
+# AuditAgent report feedback — v0.3.0
+
+- **Tool:** Nethermind **AuditAgent** (AI-generated automated scan — see the report's own *Important Notice*: "generated entirely by AI… not a full security audit… must be independently verified").
+- **Report:** `audit_agent_report_v0.3.0.pdf` (Scan ID 6, June 30 2026).
+- **Target:** branch `main`, commit `18a8e66c…07016ec8`, 12 factory contracts (868 LoC).
+- **Result reported:** 2 findings — 0 High · 0 Medium · **1 Low** · **1 Info** · 0 Best-practice.
+- **Branch triaged on:** `dev`. No fix commits land for these (see disposition); both are accepted-as-design.
+
+## Outcome
+
+**Both addressed in v0.4.0 (0 exploitable / 2 hardened).** Both findings describe the *same* intrinsic
+property of the shared counter-derived CREATE2 salt (`keccak256(cmtatCounterId)`), seen from two angles
+(cross-deployer front-running, and reentrant reuse). Neither is exploitable for fund loss or state corruption, and
+the factory already shipped custom-salt mode as the reserved-address mechanism. This release adds the two
+hardenings anyway: **NM-1** — a documentation warning steering integrators to custom-salt mode; **NM-2** — an
+OpenZeppelin `ReentrancyGuard` on every `deployCMTAT(...)` entrypoint.
+
+## Per-finding triage
+
+| ID | Severity (tool → ours) | Disposition | Status / commit |
+| --- | --- | --- | --- |
+| NM-1 | Low → Low/Info | **Accepted as design; documented** | Docs warning added (v0.4.0) |
+| NM-2 | Info → Info | **Fixed (defense-in-depth)** | `nonReentrant` on all `deployCMTAT(...)` (v0.4.0) |
+
+---
+
+### NM-1 — "Counter-derived next-address helpers are front-runnable across authorized deployers" (Low)
+
+**Claim.** With `useCustomSalt == false`, both `computedNextProxyAddress(...)` and `deployCMTAT(...)` derive the
+effective salt from the shared global `cmtatCounterId` (`nextDeploymentSalt() = keccak256(abi.encodePacked(cmtatCounterId))`,
+`CMTATFactoryRoot.sol:72-74`). Caller-supplied `deploymentSaltInput` is ignored in that mode. So if Alice predicts
+the "next" address and pre-funds it, another role-holder Bob can deploy first, advance the counter, and Alice's later
+deploy lands at a different address — stranding whatever Alice pre-sent to the predicted address.
+
+**Verdict: Accepted as design — no code change.**
+
+- The behavior is real and correctly described. It is **intrinsic to a shared sequential-counter CREATE2 scheme**:
+ in counter mode there is a single global salt sequence (`CMTATFactoryRoot.sol:85-96`), so "the next address" is a
+ property of the *factory's* next deployment, not of a specific caller. `computedNextProxyAddress` is documented as
+ exactly that — "the next proxy address using the same salt selection as `deployCMTAT`" (README L205) — i.e. valid
+ only until the next deployment by anyone.
+- **The factory already provides the mitigation: custom-salt mode (`useCustomSalt == true`).** There each caller
+ supplies its own salt, reserved one-time-use via `customSaltUsed` (`CMTATFactoryRoot.sol:86-92`); reuse reverts with
+ `CMTAT_Factory_SaltAlreadyUsed`. A specific predicted address can then be neither shifted nor stolen by another
+ deployer. So the "predict-then-pre-fund a guaranteed address" workflow **is** supported — via custom salt — and the
+ helper's caller chose the non-reserving mode.
+- **Precondition is a trusted-role threat model.** It needs ≥2 holders of `CMTAT_DEPLOYER_ROLE`, at least one
+ adversarial/negligent, and an admin that doesn't revoke. `CMTAT_DEPLOYER_ROLE` is admin-granted (`CMTATFactoryRoot.sol:33-34`),
+ not permissionless — deployment is gated by `onlyRole(CMTAT_DEPLOYER_ROLE)` (`CMTAT_UUPS_FACTORY.sol:43`).
+- **Impact is griefing of a convenience helper.** No factory-held funds are at risk and no on-chain state is corrupted;
+ pre-funding a *non-reserved* predicted address is an off-chain decision. Low (arguably Informational for this repo's
+ single-/coordinated-deployer model) is fair.
+
+**Resolution (docs, v0.4.0):** added a `WARNING` NatSpec `@dev` block to `nextDeploymentSalt()`
+(`CMTATFactoryRoot.sol`) and to `computedNextProxyAddress(...)` on all five factories, plus a warning callout in
+the README "Salt behavior" section. All state that, in counter mode, the predicted address is only valid until the
+next deployment by any authorized deployer, and that the safer mode is custom salts (`useCustomSalt == true`) with
+a unique caller-chosen, one-time-use salt that reserves the address. No behavior change — the code already exposed
+custom-salt mode as the mitigation; this makes the guidance explicit.
+
+---
+
+### NM-2 — "Counter-derived salts can be reused during reentrant proxy initialization" (Info)
+
+**Claim.** In `_deployAndRegisterProxy` (`CMTATFactoryRoot.sol:108-114`), `Create2.deploy` runs **before**
+`++cmtatCounterId`. Proxy constructors (`ERC1967Proxy` / `TransparentUpgradeableProxy` / `BeaconProxy`) execute the
+CMTAT initializer during construction. If that initializer path reenters `deployCMTAT` via a contract that also holds
+`CMTAT_DEPLOYER_ROLE`, the reentrant call observes the same un-incremented `cmtatCounterId` and derives the **same**
+automatic salt. Because the two init codes differ, the two CREATE2 addresses still differ, so both deploys succeed —
+but one salt value is reused across two events and one counter-derived salt is skipped.
+
+**Verdict: Not exploitable as described, but Fixed (defense-in-depth) in v0.4.0.**
+
+- The mechanism is correctly identified (deploy-before-increment), but the path is **not reachable with the
+ implementations these factories deploy.** Reentry requires the proxy's initializer to hand control to an
+ attacker-controlled contract that calls back into `deployCMTAT`, *and* that contract must hold
+ `CMTAT_DEPLOYER_ROLE`. The standard/light CMTAT `initialize` configures token state and stores engine addresses; it
+ does not make an outbound call into an arbitrary external address during construction that could reenter. (Verify
+ against the pinned CMTAT initializer before relying on this for any non-standard implementation passed as `logic_`.)
+- **Even if reached, on-chain invariants hold.** Each deployment still gets a **unique CREATE2 address** (distinct
+ init code ⇒ distinct address; `Create2.deploy` reverts on a genuine collision), a **unique sequential id**, and a
+ **consistent `cmtatsList`** — index == id is preserved because the matching `++cmtatCounterId` and `cmtatsList.push`
+ are paired within each call (`CMTATFactoryRoot.sol:111-113`). No fund loss, no registry corruption.
+- The only anomaly is **off-chain bookkeeping**: two `CMTATDeployed` events could carry the same `salt` field and one
+ counter-derived salt value is never used. That breaks the cosmetic "one-counter-one-salt" assumption for off-chain
+ indexers, not any on-chain safety property. Informational is fair.
+
+**Resolution (code, v0.4.0):** each concrete factory inherits OpenZeppelin `ReentrancyGuard`
+(`@openzeppelin/contracts/utils/ReentrancyGuard.sol`) — co-located with usage, in the same contract that applies
+the modifier — and marks its public `deployCMTAT(...)` entrypoint `nonReentrant onlyRole(...)` (guard first). Any
+attempt to re-enter
+`deployCMTAT(...)` during a proxy's constructor/initializer (before `++cmtatCounterId`) reverts with
+`ReentrancyGuardReentrantCall`, and consecutive automatic deployments are guaranteed distinct counter values and
+salts. Chosen over a CEI reorder because the guard preserves the `cmtatsList` index == id invariant without
+reordering deploy/emit/increment. Covered by a regression test
+(`test/UUPS/ReentrancyGuard.test.js`): a malicious `logic` mock (`contracts/mocks/ReentrancyDeployMock.sol`)
+re-enters `deployCMTAT` from the proxy initializer through a role-holding attacker — the armed case reverts and
+registers nothing, while a disarmed control deployment succeeds, isolating the guard as the cause. Full suite: 42
+passing.
+
+---
+
+## Notes on the report itself
+
+- The report is explicitly AI-generated and self-flagged as "not a full security audit" requiring independent human
+ verification — this file is that verification for the factory's own code.
+- Both findings are scoped correctly to the factory contracts (no dependency/submodule noise, unlike the unfiltered
+ Slither run). Neither overlaps the Aderyn H-1 false positive (CREATE2 init-code encoding) — see
+ [aderyn-report-feedback.md](./aderyn-report-feedback.md).
+
+## Executive triage
+
+**Neither is exploitable; both hardened in v0.4.0.** NM-1 and NM-2 are two views of the deliberate shared
+counter-derived salt design. NM-1's cross-deployer race is removed by the already-shipped custom-salt mode for
+anyone needing a reserved address — now made explicit with a documentation warning steering integrators to it. NM-2's
+reentrant reuse is unreachable with the trusted CMTAT initializers and, even hypothetically, only perturbs off-chain
+event bookkeeping while every on-chain invariant (unique address, unique id, `cmtatsList` index == id) holds — it is
+nonetheless closed defensively by an OpenZeppelin `ReentrancyGuard` (`nonReentrant`) on every factory's
+`deployCMTAT(...)` entrypoint.
diff --git a/doc/audits/v0.3.0/audit_agent_report_v0.3.0.pdf b/doc/audits/v0.3.0/audit_agent_report_v0.3.0.pdf
new file mode 100644
index 0000000..eb67863
Binary files /dev/null and b/doc/audits/v0.3.0/audit_agent_report_v0.3.0.pdf differ
diff --git a/doc/audits/v0.4.0/aderyn-report-feedback.md b/doc/audits/v0.4.0/aderyn-report-feedback.md
new file mode 100644
index 0000000..8f0589b
--- /dev/null
+++ b/doc/audits/v0.4.0/aderyn-report-feedback.md
@@ -0,0 +1,38 @@
+# Aderyn report feedback — v0.4.0
+
+- **Tool:** Aderyn 0.6.5
+- **Command:** `aderyn -x mocks --output doc/audits/v0.4.0/aderyn-report.md`
+- **Scope:** 12 factory contracts (`contracts/`, 413 nSLOC) — **mocks excluded**.
+- **Result:** 1 High · 0 Medium · 4 Low · 0 Informational.
+
+## Per-finding triage
+
+| ID | Severity | Instances | Disposition | Reason (verified against source) |
+| --- | --- | --- | --- | --- |
+| H-1 | High | 3 | **False positive** | `abi.encodePacked(type(Proxy).creationCode, abi.encode(args))` is the canonical CREATE2 init-code layout (raw creation bytecode followed by ABI-encoded constructor args). It is *not* the dangerous case the detector targets: the constructor args go through `abi.encode` (padded, unambiguous), so no two argument sets collide. `abi.encode`-ing the whole thing would produce **invalid init code** that CREATE2 cannot deploy. The same encoding is used by both `deployCMTAT` and `computedProxyAddress`, keeping prediction and deployment in sync. |
+| L-1 | Low | 6 | **By design** | `deployCMTAT` is intentionally gated by `onlyRole(CMTAT_DEPLOYER_ROLE)`, and the factory inherits OZ `AccessControl`. This permissioned deployment model is the documented design (README "Access control"); it is not a vulnerability. |
+| L-2 | Low | 11 | **By design / cosmetic** | `pragma solidity ^0.8.20;` is a deliberate caret range so the contracts can be imported as a library across compiler versions (matching upstream CMTAT). The build pins a single compiler (`0.8.34`) in `hardhat.config.js`, so deployed bytecode is deterministic. |
+| L-3 | Low | 12 | **Environment** | PUSH0 (emitted from 0.8.20+ targeting Shanghai+) may be unsupported on some L2s. `hardhat.config.js` explicitly sets `evmVersion: 'prague'`; selecting an EVM target compatible with the destination chain is a deploy-time decision, not a code defect. |
+| L-4 | Low | 2 | **False positive** | `_grantRole(DEFAULT_ADMIN_ROLE, factoryAdmin)` / `_grantRole(CMTAT_DEPLOYER_ROLE, factoryAdmin)` in the constructor ignore the returned `bool`. That return only signals whether the role was newly granted; OZ's own `AccessControl`/`Ownable` constructors ignore it during initial setup. No security impact. |
+
+## Fixed since first v0.4.0 scan
+
+- **`nonReentrant` is not the first modifier (Low, 5 instances) — FIXED.** The NM-2 reentrancy guard was first
+ declared as `onlyRole(CMTAT_DEPLOYER_ROLE) nonReentrant` on the five `deployCMTAT` entrypoints, which Aderyn
+ flagged ("the `nonReentrant` modifier should be the first modifier"). It was never a vulnerability — the only
+ preceding modifier, `onlyRole`, does a storage read + revert with no external call — but the modifiers were
+ reordered to **`nonReentrant onlyRole(CMTAT_DEPLOYER_ROLE)`** to follow the convention and clear the detector.
+ The re-run below no longer reports it. All 42 tests still pass (reordering is behaviour-neutral for legitimate
+ and reverting calls alike).
+
+## Delta from v0.3.0
+
+- v0.3.0 Aderyn reported **1 High, 4 Low** (H-1, L-1 Centralization, L-2 Unspecific pragma, L-3 PUSH0, L-4 Unchecked Return).
+- v0.4.0 final: **1 High, 4 Low — the same set as v0.3.0.** The NM-2 guard transiently introduced a fifth Low
+ (`nonReentrant` not first modifier); reordering the `deployCMTAT` modifiers removed it, so the finding IDs match
+ v0.3.0 again (L-3 = PUSH0, L-4 = Unchecked Return).
+- No new High/Medium; nothing became exploitable.
+
+## Executive triage
+
+**Nothing exploitable to fix.** The single High is a false positive on the standard CREATE2 init-code pattern; the four Lows are by-design (permissioned deployment, caret pragma), environment-dependent (PUSH0/EVM target), or a benign OZ pattern (`_grantRole` return). The lone v0.4.0-specific item — the `nonReentrant`-not-first modifier note — was resolved by reordering the `deployCMTAT` modifiers, so the current report is clean of it.
diff --git a/doc/audits/v0.4.0/aderyn-report.md b/doc/audits/v0.4.0/aderyn-report.md
new file mode 100644
index 0000000..14ddef9
--- /dev/null
+++ b/doc/audits/v0.4.0/aderyn-report.md
@@ -0,0 +1,338 @@
+
+# Aderyn report — v0.4.0
+
+- **Tool:** Aderyn 0.6.5
+- **Command:** `aderyn -x mocks --output doc/audits/v0.4.0/aderyn-report.md`
+- **Scope:** 12 factory contracts (`contracts/`, 413 nSLOC) — **mocks excluded** (`contracts/mocks/` test doubles are not analyzed).
+- **Result:** **1 High · 0 Medium · 4 Low · 0 Informational.**
+
+**Nothing exploitable to fix.** H-1 and L-1/L-2/L-3/L-4 are the same false-positive / by-design / environment items triaged in v0.3.0. The v0.4.0 NM-2 reentrancy guard originally added a cosmetic **"`nonReentrant` is not the first modifier"** finding; it was resolved by reordering the `deployCMTAT` modifiers to `nonReentrant onlyRole(...)`, so that finding is gone. See the per-finding reasoning in [`aderyn-report-feedback.md`](./aderyn-report-feedback.md) and the [audit overview](../AUDIT_OVERVIEW.md).
+
+| ID | Severity | Instances | Finding | Assessment |
+| --- | --- | --- | --- | --- |
+| H-1 | High | 3 | `abi.encodePacked()` hash collision | **False positive** — canonical CREATE2 init-code (`creationCode ++ abi.encode(args)`); not a hash-ambiguity case, and `abi.encode`-ing the whole thing would produce invalid init code |
+| L-1 | Low | 6 | Centralization risk | **By design** — deployment is gated by `CMTAT_DEPLOYER_ROLE` (documented AccessControl model) |
+| L-2 | Low | 11 | Unspecific pragma (`^0.8.20`) | **By design** — caret pragma for a reusable library; deployment pins `0.8.34` via `hardhat.config.js` |
+| L-3 | Low | 12 | PUSH0 opcode | **Environment** — `evmVersion` is explicitly set (`prague`); PUSH0 support is a per-target-chain deployer choice |
+| L-4 | Low | 2 | Unchecked return (`_grantRole`) | **False positive** — OZ pattern; the bool return is informational and ignored during constructor setup |
+
+
+
+# Aderyn Analysis Report
+
+This report was generated by [Aderyn](https://github.com/Cyfrin/aderyn), a static analysis tool built by [Cyfrin](https://cyfrin.io), a blockchain security company. This report is not a substitute for manual audit or security review. It should not be relied upon for any purpose other than to assist in the identification of potential security vulnerabilities.
+# Table of Contents
+
+- [Summary](#summary)
+ - [Files Summary](#files-summary)
+ - [Files Details](#files-details)
+ - [Issue Summary](#issue-summary)
+- [High Issues](#high-issues)
+ - [H-1: `abi.encodePacked()` Hash Collision](#h-1-abiencodepacked-hash-collision)
+- [Low Issues](#low-issues)
+ - [L-1: Centralization Risk](#l-1-centralization-risk)
+ - [L-2: Unspecific Solidity Pragma](#l-2-unspecific-solidity-pragma)
+ - [L-3: PUSH0 Opcode](#l-3-push0-opcode)
+ - [L-4: Unchecked Return](#l-4-unchecked-return)
+
+
+# Summary
+
+## Files Summary
+
+| Key | Value |
+| --- | --- |
+| .sol Files | 12 |
+| Total nSLOC | 417 |
+
+
+## Files Details
+
+| Filepath | nSLOC |
+| --- | --- |
+| contracts/libraries/CMTATBeaconFactoryBase.sol | 56 |
+| contracts/libraries/CMTATFactoryBase.sol | 12 |
+| contracts/libraries/CMTATFactoryInvariant.sol | 16 |
+| contracts/libraries/CMTATFactoryRoot.sol | 56 |
+| contracts/libraries/CMTATTransparentFactoryBase.sol | 55 |
+| contracts/libraries/ContractVersion.sol | 14 |
+| contracts/libraries/FactoryErrors.sol | 8 |
+| contracts/light/CMTAT_LIGHT_BEACON_FACTORY.sol | 38 |
+| contracts/light/CMTAT_LIGHT_TP_FACTORY.sol | 35 |
+| contracts/standard/CMTAT_BEACON_FACTORY.sol | 40 |
+| contracts/standard/CMTAT_TP_FACTORY.sol | 37 |
+| contracts/standard/CMTAT_UUPS_FACTORY.sol | 50 |
+| **Total** | **417** |
+
+
+## Issue Summary
+
+| Category | No. of Issues |
+| --- | --- |
+| High | 1 |
+| Low | 4 |
+
+
+# High Issues
+
+## H-1: `abi.encodePacked()` Hash Collision
+
+abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as `keccak256()`. Use `abi.encode()` instead which will pad items to 32 bytes, preventing hash collisions: https://docs.soliditylang.org/en/v0.8.13/abi-spec.html#non-standard-packed-mode. (e.g. `abi.encodePacked(0x123,0x456)` => `0x123456` => `abi.encodePacked(0x1,0x23456)`, but `abi.encode(0x123,0x456)` => `0x0...1230...456`). Unless there is a compelling reason, `abi.encode` should be preferred. If there is only one argument to `abi.encodePacked()` it can often be cast to `bytes()` or `bytes32()` instead: https://ethereum.stackexchange.com/questions/30912/how-to-compare-strings-in-solidity#answer-82739. If all arguments are strings and or bytes, `bytes.concat()` should be used instead.
+
+3 Found Instances
+
+
+- Found in contracts/libraries/CMTATBeaconFactoryBase.sol [Line: 92](../../../contracts/libraries/CMTATBeaconFactoryBase.sol#L92)
+
+ ```solidity
+ bytecode = abi.encodePacked(type(BeaconProxy).creationCode, abi.encode(address(beacon), initializerData));
+ ```
+
+- Found in contracts/libraries/CMTATTransparentFactoryBase.sol [Line: 88](../../../contracts/libraries/CMTATTransparentFactoryBase.sol#L88)
+
+ ```solidity
+ bytecode = abi.encodePacked(type(TransparentUpgradeableProxy).creationCode, abi.encode(logic, proxyAdminOwner, initializerData));
+ ```
+
+- Found in contracts/standard/CMTAT_UUPS_FACTORY.sol [Line: 113](../../../contracts/standard/CMTAT_UUPS_FACTORY.sol#L113)
+
+ ```solidity
+ bytecode = abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(logic, implementation));
+ ```
+
+
+
+
+
+# Low Issues
+
+## L-1: Centralization Risk
+
+Contracts have owners with privileged rights to perform admin tasks and need to be trusted to not perform malicious updates or drain funds.
+
+6 Found Instances
+
+
+- Found in contracts/libraries/CMTATFactoryRoot.sol [Line: 14](../../../contracts/libraries/CMTATFactoryRoot.sol#L14)
+
+ ```solidity
+ abstract contract CMTATFactoryRoot is AccessControl, ContractVersion, CMTATFactoryInvariant {
+ ```
+
+- Found in contracts/light/CMTAT_LIGHT_BEACON_FACTORY.sol [Line: 44](../../../contracts/light/CMTAT_LIGHT_BEACON_FACTORY.sol#L44)
+
+ ```solidity
+ ) public virtual nonReentrant onlyRole(CMTAT_DEPLOYER_ROLE) returns(BeaconProxy cmtat) {
+ ```
+
+- Found in contracts/light/CMTAT_LIGHT_TP_FACTORY.sol [Line: 37](../../../contracts/light/CMTAT_LIGHT_TP_FACTORY.sol#L37)
+
+ ```solidity
+ ) public virtual nonReentrant onlyRole(CMTAT_DEPLOYER_ROLE) returns(TransparentUpgradeableProxy cmtat) {
+ ```
+
+- Found in contracts/standard/CMTAT_BEACON_FACTORY.sol [Line: 55](../../../contracts/standard/CMTAT_BEACON_FACTORY.sol#L55)
+
+ ```solidity
+ ) public virtual nonReentrant onlyRole(CMTAT_DEPLOYER_ROLE) returns(BeaconProxy cmtat) {
+ ```
+
+- Found in contracts/standard/CMTAT_TP_FACTORY.sol [Line: 46](../../../contracts/standard/CMTAT_TP_FACTORY.sol#L46)
+
+ ```solidity
+ ) public virtual nonReentrant onlyRole(CMTAT_DEPLOYER_ROLE) returns(TransparentUpgradeableProxy cmtat) {
+ ```
+
+- Found in contracts/standard/CMTAT_UUPS_FACTORY.sol [Line: 44](../../../contracts/standard/CMTAT_UUPS_FACTORY.sol#L44)
+
+ ```solidity
+ ) public virtual nonReentrant onlyRole(CMTAT_DEPLOYER_ROLE) returns(ERC1967Proxy cmtat) {
+ ```
+
+
+
+
+
+## L-2: Unspecific Solidity Pragma
+
+Consider using a specific version of Solidity in your contracts instead of a wide version. For example, instead of `pragma solidity ^0.8.0;`, use `pragma solidity 0.8.0;`
+
+11 Found Instances
+
+
+- Found in contracts/libraries/CMTATBeaconFactoryBase.sol [Line: 2](../../../contracts/libraries/CMTATBeaconFactoryBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/libraries/CMTATFactoryBase.sol [Line: 2](../../../contracts/libraries/CMTATFactoryBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/libraries/CMTATFactoryInvariant.sol [Line: 2](../../../contracts/libraries/CMTATFactoryInvariant.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/libraries/CMTATFactoryRoot.sol [Line: 2](../../../contracts/libraries/CMTATFactoryRoot.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/libraries/CMTATTransparentFactoryBase.sol [Line: 2](../../../contracts/libraries/CMTATTransparentFactoryBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/libraries/ContractVersion.sol [Line: 2](../../../contracts/libraries/ContractVersion.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/light/CMTAT_LIGHT_BEACON_FACTORY.sol [Line: 2](../../../contracts/light/CMTAT_LIGHT_BEACON_FACTORY.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/light/CMTAT_LIGHT_TP_FACTORY.sol [Line: 2](../../../contracts/light/CMTAT_LIGHT_TP_FACTORY.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/standard/CMTAT_BEACON_FACTORY.sol [Line: 2](../../../contracts/standard/CMTAT_BEACON_FACTORY.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/standard/CMTAT_TP_FACTORY.sol [Line: 2](../../../contracts/standard/CMTAT_TP_FACTORY.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/standard/CMTAT_UUPS_FACTORY.sol [Line: 2](../../../contracts/standard/CMTAT_UUPS_FACTORY.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+
+
+
+
+## L-3: PUSH0 Opcode
+
+Solc compiler version 0.8.20 switches the default target EVM version to Shanghai, which means that the generated bytecode will include PUSH0 opcodes. Be sure to select the appropriate EVM version in case you intend to deploy on a chain other than mainnet like L2 chains that may not support PUSH0, otherwise deployment of your contracts will fail.
+
+12 Found Instances
+
+
+- Found in contracts/libraries/CMTATBeaconFactoryBase.sol [Line: 2](../../../contracts/libraries/CMTATBeaconFactoryBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/libraries/CMTATFactoryBase.sol [Line: 2](../../../contracts/libraries/CMTATFactoryBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/libraries/CMTATFactoryInvariant.sol [Line: 2](../../../contracts/libraries/CMTATFactoryInvariant.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/libraries/CMTATFactoryRoot.sol [Line: 2](../../../contracts/libraries/CMTATFactoryRoot.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/libraries/CMTATTransparentFactoryBase.sol [Line: 2](../../../contracts/libraries/CMTATTransparentFactoryBase.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/libraries/ContractVersion.sol [Line: 2](../../../contracts/libraries/ContractVersion.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/libraries/FactoryErrors.sol [Line: 3](../../../contracts/libraries/FactoryErrors.sol#L3)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/light/CMTAT_LIGHT_BEACON_FACTORY.sol [Line: 2](../../../contracts/light/CMTAT_LIGHT_BEACON_FACTORY.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/light/CMTAT_LIGHT_TP_FACTORY.sol [Line: 2](../../../contracts/light/CMTAT_LIGHT_TP_FACTORY.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/standard/CMTAT_BEACON_FACTORY.sol [Line: 2](../../../contracts/standard/CMTAT_BEACON_FACTORY.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/standard/CMTAT_TP_FACTORY.sol [Line: 2](../../../contracts/standard/CMTAT_TP_FACTORY.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+- Found in contracts/standard/CMTAT_UUPS_FACTORY.sol [Line: 2](../../../contracts/standard/CMTAT_UUPS_FACTORY.sol#L2)
+
+ ```solidity
+ pragma solidity ^0.8.20;
+ ```
+
+
+
+
+
+## L-4: Unchecked Return
+
+Function returns a value but it is ignored. Consider checking the return value.
+
+2 Found Instances
+
+
+- Found in contracts/libraries/CMTATFactoryRoot.sol [Line: 33](../../../contracts/libraries/CMTATFactoryRoot.sol#L33)
+
+ ```solidity
+ _grantRole(DEFAULT_ADMIN_ROLE, factoryAdmin);
+ ```
+
+- Found in contracts/libraries/CMTATFactoryRoot.sol [Line: 34](../../../contracts/libraries/CMTATFactoryRoot.sol#L34)
+
+ ```solidity
+ _grantRole(CMTAT_DEPLOYER_ROLE, factoryAdmin);
+ ```
+
+
+
+
+
diff --git a/doc/audits/v0.4.0/slither-report-feedback.md b/doc/audits/v0.4.0/slither-report-feedback.md
new file mode 100644
index 0000000..6c14adb
--- /dev/null
+++ b/doc/audits/v0.4.0/slither-report-feedback.md
@@ -0,0 +1,38 @@
+# Slither report feedback — v0.4.0
+
+- **Tool:** Slither 0.11.5
+- **Command:** `slither . --checklist --filter-paths "node_modules,CMTAT,test,forge-std,mocks"`
+- **Scope:** factory contracts only — **mocks excluded**; `node_modules` and the vendored `CMTAT/` submodule filtered out.
+
+## Result
+
+The filtered checklist is **empty**: Slither finds no issue whose primary location is in the factory's own contracts (`contracts/standard`, `contracts/light`, `contracts/libraries`).
+
+An unfiltered run surfaces detectors that all resolve to dependencies, the CMTAT submodule, or the excluded `contracts/mocks/` test doubles. Each was verified by opening the cited `file:line`; the filtered run returning 0 confirms none are primary-located in the maintained factory code.
+
+| Detector | Severity | Primary location (verified) | Disposition | Reason |
+| --- | --- | --- | --- | --- |
+| incorrect-return | High | `node_modules/@openzeppelin/contracts/proxy/Proxy.sol` | False positive | OZ proxy `_delegate` uses an assembly `return`; Slither misreads it. Dependency code, not ours. |
+| locked-ether | Medium | `contracts/mocks/ReentrancyDeployMock.sol` | Mock-only | `ReentrantInitLogicMock` has `payable` `receive`/`fallback` for the reentrancy test; it is test scaffolding, excluded from the deployed scope. |
+| uninitialized-local | Medium | `CMTAT/` submodule | Out of scope | Vendored CMTAT submodule. |
+| unused-return | Medium | `node_modules/@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol` | Out of scope | OZ dependency. |
+| shadowing-local | Low | `CMTAT/` submodule | Out of scope | Vendored CMTAT submodule. |
+| missing-zero-check | Low | 2× OZ proxies + 2× `contracts/mocks/ReentrancyDeployMock.sol` | Out of scope / Mock-only | The OZ instances are dependency code; the two new ones are address params in the test-only attacker/logic mocks. The factory *does* validate its own zero addresses via `FactoryErrors`. |
+| calls-loop | Low | `node_modules` / `CMTAT/` | Out of scope | Dependency / submodule. |
+| assembly | Info | `node_modules` / `CMTAT/` (+1 mock) | Out of scope / Mock-only | Dependency / submodule; the +1 is the mock's `revert` bubbling assembly. |
+| pragma | Info | project-wide version list | Cosmetic | Multiple Solidity versions across the dependency tree; not fixable in the factory. |
+| dead-code | Info | `node_modules` / `CMTAT/` | Out of scope | Dependency / submodule. |
+| solc-version | Info | project-wide version list | Cosmetic | Caret pragmas in deps; deployment pins `0.8.34`. |
+| low-level-calls | Info | `contracts/mocks/ReentrancyDeployMock.sol` | Mock-only | `factory.call(reentrantCall)` in the reentrancy attacker double; test scaffolding only. |
+| naming-convention | Info | `node_modules` / `CMTAT/` (+1 mock) | Out of scope / Mock-only | Dependency / submodule; +1 from the mock's `ATTACKER` immutable. |
+| too-many-digits | Info | `node_modules` / `CMTAT/` | Out of scope | Dependency / submodule. |
+| unindexed-event-address | Info | `node_modules` / `CMTAT/` | Out of scope | Dependency / submodule. The factory's own `CMTATDeployed` indexes `proxy`, `deployer`, and `id`. |
+
+## Delta from v0.3.0
+
+- The **factory-scoped result is unchanged: 0 findings** (same as v0.3.0 and v0.2.0).
+- The only difference in the *unfiltered* view is the new `contracts/mocks/ReentrancyDeployMock.sol` test double added for the NM-2 regression test: it introduces `locked-ether` (1), `low-level-calls` (1), +2 `missing-zero-check`, and +1 each to `assembly` / `naming-convention`. All are mock-only and excluded by the scoped `--filter-paths … ,mocks` run.
+
+## Executive triage
+
+**Nothing to fix.** No Slither finding is exploitable or located in the factory's maintained code. The factory contracts validate their own zero-address inputs (`FactoryErrors.*`) and index their deployment event, so the dependency-level and mock-only detectors do not translate into project issues.
diff --git a/doc/audits/v0.4.0/slither-report.md b/doc/audits/v0.4.0/slither-report.md
new file mode 100644
index 0000000..2d0b4d6
--- /dev/null
+++ b/doc/audits/v0.4.0/slither-report.md
@@ -0,0 +1,36 @@
+
+# Slither report — v0.4.0
+
+- **Tool:** Slither 0.11.5
+- **Command:** `slither . --checklist --filter-paths "node_modules,CMTAT,test,forge-std,mocks"`
+- **Scope:** factory contracts only — **mocks excluded**, dependencies (`node_modules`) and the vendored `CMTAT/` submodule filtered out.
+- **Result (factory contracts):** **0 High · 0 Medium · 0 Low · 0 Informational** — the filtered checklist below is empty.
+
+**Nothing to fix.** Slither reports no finding whose primary location is inside the factory's own contracts (`contracts/standard`, `contracts/light`, `contracts/libraries`).
+
+For context, an *unfiltered* run (`slither . --checklist`, including dependencies, the CMTAT submodule, and the excluded `contracts/mocks/` test doubles) surfaces the detectors below — every one is located outside this repo's maintained factory code. This was confirmed by the filtered run returning 0.
+
+| Detector | Severity | Instances (unfiltered) | Primary location | Assessment |
+| --- | --- | --- | --- | --- |
+| incorrect-return | High | 1 | `node_modules` OZ `Proxy.sol` (assembly `return`) | Out of scope — OZ false positive |
+| locked-ether | Medium | 1 | `contracts/mocks/ReentrancyDeployMock.sol` | **Mock-only** — `payable` test double, no production impact (excluded from scope) |
+| uninitialized-local | Medium | 1 | `CMTAT/` submodule | Out of scope |
+| unused-return | Medium | 2 | `node_modules` OZ `ERC1967Utils` | Out of scope |
+| shadowing-local | Low | 1 | `CMTAT/` submodule | Out of scope |
+| missing-zero-check | Low | 4 | 2× `node_modules` OZ proxies + 2× `contracts/mocks/` | Out of scope / **mock-only** |
+| calls-loop | Low | 8 | `node_modules` / `CMTAT/` | Out of scope |
+| assembly | Info | 35 | `node_modules` / `CMTAT/` (+1 mock) | Out of scope / mock-only |
+| pragma | Info | 1 | project-wide version list | Cosmetic (multi-version pragma across the dep tree) |
+| dead-code | Info | 2 | `node_modules` / `CMTAT/` | Out of scope |
+| solc-version | Info | 7 | project-wide version list | Cosmetic |
+| low-level-calls | Info | 1 | `contracts/mocks/ReentrancyDeployMock.sol` | **Mock-only** — `factory.call(...)` in the reentrancy test double |
+| naming-convention | Info | 56 | `node_modules` / `CMTAT/` (+1 mock) | Out of scope / mock-only |
+| too-many-digits | Info | 2 | `node_modules` / `CMTAT/` | Out of scope |
+| unindexed-event-address | Info | 3 | `node_modules` / `CMTAT/` | Out of scope |
+
+The v0.4.0 additions vs v0.3.0 (`locked-ether`, `low-level-calls`, +2 `missing-zero-check`, +1 each `assembly`/`naming-convention`) are all in the new `contracts/mocks/ReentrancyDeployMock.sol` test double, which the scoped run excludes. See the triage in [`slither-report-feedback.md`](./slither-report-feedback.md) and the [audit overview](../AUDIT_OVERVIEW.md).
+
+
+
+**THIS CHECKLIST IS NOT COMPLETE**. Use `--show-ignored-findings` to show all the results.
+Summary
diff --git a/doc/schema/surya_graph/surya_graph_CMTATFactoryRoot.sol.png b/doc/schema/surya_graph/surya_graph_CMTATFactoryRoot.sol.png
index fd8252c..7ca7b7e 100644
Binary files a/doc/schema/surya_graph/surya_graph_CMTATFactoryRoot.sol.png and b/doc/schema/surya_graph/surya_graph_CMTATFactoryRoot.sol.png differ
diff --git a/doc/schema/surya_graph/surya_graph_ContractVersion.sol.png b/doc/schema/surya_graph/surya_graph_ContractVersion.sol.png
new file mode 100644
index 0000000..010f521
Binary files /dev/null and b/doc/schema/surya_graph/surya_graph_ContractVersion.sol.png differ
diff --git a/doc/schema/surya_graph/surya_graph_ReentrancyDeployMock.sol.png b/doc/schema/surya_graph/surya_graph_ReentrancyDeployMock.sol.png
new file mode 100644
index 0000000..b169113
Binary files /dev/null and b/doc/schema/surya_graph/surya_graph_ReentrancyDeployMock.sol.png differ
diff --git a/doc/schema/surya_inheritance/surya_inheritance_CMTAT_BEACON_FACTORY.sol.png b/doc/schema/surya_inheritance/surya_inheritance_CMTAT_BEACON_FACTORY.sol.png
index 406c424..be0f918 100644
Binary files a/doc/schema/surya_inheritance/surya_inheritance_CMTAT_BEACON_FACTORY.sol.png and b/doc/schema/surya_inheritance/surya_inheritance_CMTAT_BEACON_FACTORY.sol.png differ
diff --git a/doc/schema/surya_inheritance/surya_inheritance_CMTAT_LIGHT_BEACON_FACTORY.sol.png b/doc/schema/surya_inheritance/surya_inheritance_CMTAT_LIGHT_BEACON_FACTORY.sol.png
index 4a6dac9..b8a0a56 100644
Binary files a/doc/schema/surya_inheritance/surya_inheritance_CMTAT_LIGHT_BEACON_FACTORY.sol.png and b/doc/schema/surya_inheritance/surya_inheritance_CMTAT_LIGHT_BEACON_FACTORY.sol.png differ
diff --git a/doc/schema/surya_inheritance/surya_inheritance_CMTAT_LIGHT_TP_FACTORY.sol.png b/doc/schema/surya_inheritance/surya_inheritance_CMTAT_LIGHT_TP_FACTORY.sol.png
index 69e3a86..47260ca 100644
Binary files a/doc/schema/surya_inheritance/surya_inheritance_CMTAT_LIGHT_TP_FACTORY.sol.png and b/doc/schema/surya_inheritance/surya_inheritance_CMTAT_LIGHT_TP_FACTORY.sol.png differ
diff --git a/doc/schema/surya_inheritance/surya_inheritance_CMTAT_TP_FACTORY.sol.png b/doc/schema/surya_inheritance/surya_inheritance_CMTAT_TP_FACTORY.sol.png
index fc51082..7f52824 100644
Binary files a/doc/schema/surya_inheritance/surya_inheritance_CMTAT_TP_FACTORY.sol.png and b/doc/schema/surya_inheritance/surya_inheritance_CMTAT_TP_FACTORY.sol.png differ
diff --git a/doc/schema/surya_inheritance/surya_inheritance_CMTAT_UUPS_FACTORY.sol.png b/doc/schema/surya_inheritance/surya_inheritance_CMTAT_UUPS_FACTORY.sol.png
index 3eb92ee..7a8b435 100644
Binary files a/doc/schema/surya_inheritance/surya_inheritance_CMTAT_UUPS_FACTORY.sol.png and b/doc/schema/surya_inheritance/surya_inheritance_CMTAT_UUPS_FACTORY.sol.png differ
diff --git a/doc/schema/surya_inheritance/surya_inheritance_ReentrancyDeployMock.sol.png b/doc/schema/surya_inheritance/surya_inheritance_ReentrancyDeployMock.sol.png
new file mode 100644
index 0000000..9834c1c
Binary files /dev/null and b/doc/schema/surya_inheritance/surya_inheritance_ReentrancyDeployMock.sol.png differ
diff --git a/doc/schema/surya_report/surya_report_CMTATBeaconFactoryBase.sol.md b/doc/schema/surya_report/surya_report_CMTATBeaconFactoryBase.sol.md
new file mode 100644
index 0000000..5bffb0f
--- /dev/null
+++ b/doc/schema/surya_report/surya_report_CMTATBeaconFactoryBase.sol.md
@@ -0,0 +1,33 @@
+## Sūrya's Description Report
+
+### Files Description Table
+
+
+| File Name | SHA-1 Hash |
+|-------------|--------------|
+| ./libraries/CMTATBeaconFactoryBase.sol | [object Promise] |
+
+
+### Contracts Description Table
+
+
+| Contract | Type | Bases | | |
+|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
+| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
+||||||
+| **CMTATBeaconFactoryBase** | Implementation | CMTATFactoryRoot |||
+| └ | | Public ❗️ | 🛑 | CMTATFactoryRoot |
+| └ | implementation | Public ❗️ | |NO❗️ |
+| └ | _deployBeaconProxy | Internal 🔒 | 🛑 | |
+| └ | _computedBeaconProxyAddress | Internal 🔒 | | |
+| └ | _computedNextBeaconProxyAddress | Internal 🔒 | | |
+| └ | _deployBeaconProxyBytecode | Internal 🔒 | 🛑 | |
+| └ | _getBeaconProxyBytecode | Internal 🔒 | | |
+
+
+### Legend
+
+| Symbol | Meaning |
+|:--------:|-----------|
+| 🛑 | Function can modify state |
+| 💵 | Function is payable |
diff --git a/doc/schema/surya_report/surya_report_CMTATFactoryRoot.sol.md b/doc/schema/surya_report/surya_report_CMTATFactoryRoot.sol.md
index f6cae3f..34fabdb 100644
--- a/doc/schema/surya_report/surya_report_CMTATFactoryRoot.sol.md
+++ b/doc/schema/surya_report/surya_report_CMTATFactoryRoot.sol.md
@@ -15,10 +15,14 @@
|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
||||||
-| **CMTATFactoryRoot** | Implementation | AccessControl, CMTATFactoryInvariant |||
+| **CMTATFactoryRoot** | Implementation | AccessControl, ContractVersion, CMTATFactoryInvariant |||
| └ | | Public ❗️ | 🛑 |NO❗️ |
| └ | CMTATProxyAddress | Public ❗️ | |NO❗️ |
+| └ | supportsInterface | Public ❗️ | |NO❗️ |
+| └ | nextDeploymentSalt | Public ❗️ | |NO❗️ |
| └ | _checkAndDetermineDeploymentSalt | Internal 🔒 | 🛑 | |
+| └ | _computeDeploymentSalt | Internal 🔒 | | |
+| └ | _deployAndRegisterProxy | Internal 🔒 | 🛑 | |
### Legend
diff --git a/doc/schema/surya_report/surya_report_CMTATTransparentFactoryBase.sol.md b/doc/schema/surya_report/surya_report_CMTATTransparentFactoryBase.sol.md
new file mode 100644
index 0000000..ca645f2
--- /dev/null
+++ b/doc/schema/surya_report/surya_report_CMTATTransparentFactoryBase.sol.md
@@ -0,0 +1,33 @@
+## Sūrya's Description Report
+
+### Files Description Table
+
+
+| File Name | SHA-1 Hash |
+|-------------|--------------|
+| ./libraries/CMTATTransparentFactoryBase.sol | [object Promise] |
+
+
+### Contracts Description Table
+
+
+| Contract | Type | Bases | | |
+|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
+| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
+||||||
+| **CMTATTransparentFactoryBase** | Implementation | CMTATFactoryBase |||
+| └ | | Public ❗️ | 🛑 | CMTATFactoryBase |
+| └ | _deployTransparentProxy | Internal 🔒 | 🛑 | |
+| └ | _computedTransparentProxyAddress | Internal 🔒 | | |
+| └ | _computedNextTransparentProxyAddress | Internal 🔒 | | |
+| └ | _deployTransparentProxyBytecode | Internal 🔒 | 🛑 | |
+| └ | _checkProxyAdminOwner | Internal 🔒 | | |
+| └ | _getTransparentProxyBytecode | Internal 🔒 | | |
+
+
+### Legend
+
+| Symbol | Meaning |
+|:--------:|-----------|
+| 🛑 | Function can modify state |
+| 💵 | Function is payable |
diff --git a/doc/schema/surya_report/surya_report_CMTAT_BEACON_FACTORY.sol.md b/doc/schema/surya_report/surya_report_CMTAT_BEACON_FACTORY.sol.md
index 1346b4a..913a3b5 100644
--- a/doc/schema/surya_report/surya_report_CMTAT_BEACON_FACTORY.sol.md
+++ b/doc/schema/surya_report/surya_report_CMTAT_BEACON_FACTORY.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./CMTAT_BEACON_FACTORY.sol | [object Promise] |
+| ./standard/CMTAT_BEACON_FACTORY.sol | [object Promise] |
### Contracts Description Table
@@ -15,13 +15,12 @@
|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
||||||
-| **CMTAT_BEACON_FACTORY** | Implementation | AccessControl, CMTATFactoryRoot |||
-| └ | | Public ❗️ | 🛑 | CMTATFactoryRoot |
-| └ | deployCMTAT | Public ❗️ | 🛑 | onlyRole |
+| **CMTAT_BEACON_FACTORY** | Implementation | CMTATBeaconFactoryBase, ReentrancyGuard |||
+| └ | | Public ❗️ | 🛑 | CMTATBeaconFactoryBase |
+| └ | deployCMTAT | Public ❗️ | 🛑 | nonReentrant onlyRole |
| └ | computedProxyAddress | Public ❗️ | |NO❗️ |
-| └ | implementation | Public ❗️ | |NO❗️ |
-| └ | _deployBytecode | Internal 🔒 | 🛑 | |
-| └ | _getBytecode | Internal 🔒 | | |
+| └ | computedNextProxyAddress | Public ❗️ | |NO❗️ |
+| └ | _initializerData | Internal 🔒 | | |
### Legend
diff --git a/doc/schema/surya_report/surya_report_CMTAT_LIGHT_BEACON_FACTORY.sol.md b/doc/schema/surya_report/surya_report_CMTAT_LIGHT_BEACON_FACTORY.sol.md
new file mode 100644
index 0000000..5a31ab7
--- /dev/null
+++ b/doc/schema/surya_report/surya_report_CMTAT_LIGHT_BEACON_FACTORY.sol.md
@@ -0,0 +1,31 @@
+## Sūrya's Description Report
+
+### Files Description Table
+
+
+| File Name | SHA-1 Hash |
+|-------------|--------------|
+| ./light/CMTAT_LIGHT_BEACON_FACTORY.sol | [object Promise] |
+
+
+### Contracts Description Table
+
+
+| Contract | Type | Bases | | |
+|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
+| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
+||||||
+| **CMTAT_LIGHT_BEACON_FACTORY** | Implementation | CMTATBeaconFactoryBase, ReentrancyGuard |||
+| └ | | Public ❗️ | 🛑 | CMTATBeaconFactoryBase |
+| └ | deployCMTAT | Public ❗️ | 🛑 | nonReentrant onlyRole |
+| └ | computedProxyAddress | Public ❗️ | |NO❗️ |
+| └ | computedNextProxyAddress | Public ❗️ | |NO❗️ |
+| └ | _initializerData | Internal 🔒 | | |
+
+
+### Legend
+
+| Symbol | Meaning |
+|:--------:|-----------|
+| 🛑 | Function can modify state |
+| 💵 | Function is payable |
diff --git a/doc/schema/surya_report/surya_report_CMTAT_LIGHT_TP_FACTORY.sol.md b/doc/schema/surya_report/surya_report_CMTAT_LIGHT_TP_FACTORY.sol.md
new file mode 100644
index 0000000..bfcd168
--- /dev/null
+++ b/doc/schema/surya_report/surya_report_CMTAT_LIGHT_TP_FACTORY.sol.md
@@ -0,0 +1,31 @@
+## Sūrya's Description Report
+
+### Files Description Table
+
+
+| File Name | SHA-1 Hash |
+|-------------|--------------|
+| ./light/CMTAT_LIGHT_TP_FACTORY.sol | [object Promise] |
+
+
+### Contracts Description Table
+
+
+| Contract | Type | Bases | | |
+|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
+| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
+||||||
+| **CMTAT_LIGHT_TP_FACTORY** | Implementation | CMTATTransparentFactoryBase, ReentrancyGuard |||
+| └ | | Public ❗️ | 🛑 | CMTATTransparentFactoryBase |
+| └ | deployCMTAT | Public ❗️ | 🛑 | nonReentrant onlyRole |
+| └ | computedProxyAddress | Public ❗️ | |NO❗️ |
+| └ | computedNextProxyAddress | Public ❗️ | |NO❗️ |
+| └ | _initializerData | Internal 🔒 | | |
+
+
+### Legend
+
+| Symbol | Meaning |
+|:--------:|-----------|
+| 🛑 | Function can modify state |
+| 💵 | Function is payable |
diff --git a/doc/schema/surya_report/surya_report_CMTAT_TP_FACTORY.sol.md b/doc/schema/surya_report/surya_report_CMTAT_TP_FACTORY.sol.md
index 0b8c949..221c8a0 100644
--- a/doc/schema/surya_report/surya_report_CMTAT_TP_FACTORY.sol.md
+++ b/doc/schema/surya_report/surya_report_CMTAT_TP_FACTORY.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./CMTAT_TP_FACTORY.sol | [object Promise] |
+| ./standard/CMTAT_TP_FACTORY.sol | [object Promise] |
### Contracts Description Table
@@ -15,12 +15,12 @@
|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
||||||
-| **CMTAT_TP_FACTORY** | Implementation | CMTATFactoryBase |||
-| └ | | Public ❗️ | 🛑 | CMTATFactoryBase |
-| └ | deployCMTAT | Public ❗️ | 🛑 | onlyRole |
+| **CMTAT_TP_FACTORY** | Implementation | CMTATTransparentFactoryBase, ReentrancyGuard |||
+| └ | | Public ❗️ | 🛑 | CMTATTransparentFactoryBase |
+| └ | deployCMTAT | Public ❗️ | 🛑 | nonReentrant onlyRole |
| └ | computedProxyAddress | Public ❗️ | |NO❗️ |
-| └ | _deployBytecode | Internal 🔒 | 🛑 | |
-| └ | _getBytecode | Internal 🔒 | | |
+| └ | computedNextProxyAddress | Public ❗️ | |NO❗️ |
+| └ | _initializerData | Internal 🔒 | | |
### Legend
diff --git a/doc/schema/surya_report/surya_report_CMTAT_UUPS_FACTORY.sol.md b/doc/schema/surya_report/surya_report_CMTAT_UUPS_FACTORY.sol.md
index d2ce39e..d8d2598 100644
--- a/doc/schema/surya_report/surya_report_CMTAT_UUPS_FACTORY.sol.md
+++ b/doc/schema/surya_report/surya_report_CMTAT_UUPS_FACTORY.sol.md
@@ -5,7 +5,7 @@
| File Name | SHA-1 Hash |
|-------------|--------------|
-| ./CMTAT_UUPS_FACTORY.sol | [object Promise] |
+| ./standard/CMTAT_UUPS_FACTORY.sol | [object Promise] |
### Contracts Description Table
@@ -15,10 +15,11 @@
|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
||||||
-| **CMTAT_UUPS_FACTORY** | Implementation | CMTATFactoryBase |||
+| **CMTAT_UUPS_FACTORY** | Implementation | CMTATFactoryBase, ReentrancyGuard |||
| └ | | Public ❗️ | 🛑 | CMTATFactoryBase |
-| └ | deployCMTAT | Public ❗️ | 🛑 | onlyRole |
+| └ | deployCMTAT | Public ❗️ | 🛑 | nonReentrant onlyRole |
| └ | computedProxyAddress | Public ❗️ | |NO❗️ |
+| └ | computedNextProxyAddress | Public ❗️ | |NO❗️ |
| └ | _deployBytecode | Internal 🔒 | 🛑 | |
| └ | _getBytecode | Internal 🔒 | | |
diff --git a/doc/schema/surya_report/surya_report_ContractVersion.sol.md b/doc/schema/surya_report/surya_report_ContractVersion.sol.md
new file mode 100644
index 0000000..4a6547e
--- /dev/null
+++ b/doc/schema/surya_report/surya_report_ContractVersion.sol.md
@@ -0,0 +1,31 @@
+## Sūrya's Description Report
+
+### Files Description Table
+
+
+| File Name | SHA-1 Hash |
+|-------------|--------------|
+| ./libraries/ContractVersion.sol | [object Promise] |
+
+
+### Contracts Description Table
+
+
+| Contract | Type | Bases | | |
+|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
+| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
+||||||
+| **IERC8303** | Interface | |||
+| └ | version | External ❗️ | |NO❗️ |
+||||||
+| **ContractVersion** | Implementation | ERC165, IERC8303 |||
+| └ | version | Public ❗️ | |NO❗️ |
+| └ | supportsInterface | Public ❗️ | |NO❗️ |
+
+
+### Legend
+
+| Symbol | Meaning |
+|:--------:|-----------|
+| 🛑 | Function can modify state |
+| 💵 | Function is payable |
diff --git a/doc/schema/surya_report/surya_report_ReentrancyDeployMock.sol.md b/doc/schema/surya_report/surya_report_ReentrancyDeployMock.sol.md
new file mode 100644
index 0000000..7513d25
--- /dev/null
+++ b/doc/schema/surya_report/surya_report_ReentrancyDeployMock.sol.md
@@ -0,0 +1,33 @@
+## Sūrya's Description Report
+
+### Files Description Table
+
+
+| File Name | SHA-1 Hash |
+|-------------|--------------|
+| ./mocks/ReentrancyDeployMock.sol | [object Promise] |
+
+
+### Contracts Description Table
+
+
+| Contract | Type | Bases | | |
+|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:|
+| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** |
+||||||
+| **ReentrancyDeployAttacker** | Implementation | |||
+| └ | configure | External ❗️ | 🛑 |NO❗️ |
+| └ | attack | External ❗️ | 🛑 |NO❗️ |
+||||||
+| **ReentrantInitLogicMock** | Implementation | |||
+| └ | | Public ❗️ | 🛑 |NO❗️ |
+| └ | | External ❗️ | 💵 |NO❗️ |
+| └ | | External ❗️ | 💵 |NO❗️ |
+
+
+### Legend
+
+| Symbol | Meaning |
+|:--------:|-----------|
+| 🛑 | Function can modify state |
+| 💵 | Function is payable |
diff --git a/doc/script/convert_links_for_pdf.sh b/doc/script/convert_links_for_pdf.sh
new file mode 100755
index 0000000..2f4d392
--- /dev/null
+++ b/doc/script/convert_links_for_pdf.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+# Script to replace relative markdown links with GitHub links for PDF generation
+# Preserves image links (they render in PDF) and external links
+
+set -e
+
+if [ -z "$1" ]; then
+ echo "Usage: $0 [input-file] [output-file]"
+ echo ""
+ echo "Example:"
+ echo " $0 https://github.com/CMTA/CMTAT/blob/v3.0.0"
+ echo " $0 https://github.com/CMTA/CMTAT/blob/v3.0.0 README.md README_UPDATE.md"
+ exit 1
+fi
+
+GITHUB_LINK="${1%/}" # Remove trailing slash if present
+INPUT_FILE="${2:-../../README.md}"
+OUTPUT_FILE="${3:-README_UPDATE.md}"
+
+if [ ! -f "$INPUT_FILE" ]; then
+ echo "Error: Input file '$INPUT_FILE' not found"
+ exit 1
+fi
+
+# Create a temporary file
+TMP_FILE=$(mktemp)
+cp "$INPUT_FILE" "$TMP_FILE"
+
+# Use a placeholder to avoid sed escaping issues
+PLACEHOLDER="__GITHUB_LINK__"
+
+# Step 1: Convert ALL relative links [text](./...) to placeholder
+sed -i -E "s|\[([^]]+)\]\(\./([^)]+)\)|[\1]($PLACEHOLDER/\2)|g" "$TMP_FILE"
+
+# Step 2: Restore image links back to relative (images render inline in PDF)
+for ext in png jpg jpeg gif svg ico webp bmp tiff; do
+ sed -i -E "s|\[([^]]+)\]\($PLACEHOLDER/([^)]+\.$ext)\)|[\1](./\2)|gi" "$TMP_FILE"
+done
+
+# Step 3: Replace placeholder with actual GitHub link
+sed -i "s|$PLACEHOLDER|$GITHUB_LINK|g" "$TMP_FILE"
+
+mv "$TMP_FILE" "$OUTPUT_FILE"
+
+echo "Created '$OUTPUT_FILE' with GitHub links pointing to:"
+echo " $GITHUB_LINK"
diff --git a/doc/specification/coverpage.odg b/doc/specification/coverpage.odg
new file mode 100644
index 0000000..bc6795a
Binary files /dev/null and b/doc/specification/coverpage.odg differ
diff --git a/package-lock.json b/package-lock.json
index 442a480..2624610 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "CMTAT Factory",
- "version": "0.3.0",
+ "version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "CMTAT Factory",
- "version": "0.3.0",
+ "version": "0.4.0",
"license": "MPL",
"dependencies": {
"@openzeppelin/contracts": "5.6.1",
diff --git a/package.json b/package.json
index 05a59c5..de07ef2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "CMTAT Factory",
- "version": "0.3.0",
+ "version": "0.4.0",
"description": "Factory smart contracts for CMTAT",
"files": [
"contracts",
diff --git a/test/ContractVersion.test.js b/test/ContractVersion.test.js
index 5dba4fa..3c8da61 100644
--- a/test/ContractVersion.test.js
+++ b/test/ContractVersion.test.js
@@ -10,7 +10,7 @@ const ERC8303_INTERFACE_ID = '0x54fd4d50'
const ERC165_INTERFACE_ID = '0x01ffc9a7'
const ACCESS_CONTROL_INTERFACE_ID = '0x7965db0b'
const INVALID_INTERFACE_ID = '0xffffffff'
-const FACTORY_VERSION = '0.3.0'
+const FACTORY_VERSION = '0.4.0'
describe('Factory ContractVersion', function () {
beforeEach(async function () {
diff --git a/test/UUPS/ReentrancyGuard.test.js b/test/UUPS/ReentrancyGuard.test.js
new file mode 100644
index 0000000..c6a8016
--- /dev/null
+++ b/test/UUPS/ReentrancyGuard.test.js
@@ -0,0 +1,89 @@
+const { expect } = require('chai')
+const {
+ ZERO_ADDRESS,
+ CMTAT_DEPLOYER_ROLE,
+ extraInformationAttributes
+} = require('../utils.js')
+const { fixture, loadFixture } = require('../../CMTAT/test/deploymentUtils.js')
+const { ethers } = require('hardhat')
+
+const DEPLOYMENT_DECIMAL = 0
+
+// Regression test for Nethermind AuditAgent NM-2: a deployCMTAT re-entry triggered from a proxy's
+// initializer (before cmtatCounterId is incremented) must be rejected by the nonReentrant guard on
+// the public deployCMTAT entrypoints (guard state inherited from CMTATFactoryRoot / OZ ReentrancyGuard).
+describe('Factory reentrancy guard (AuditAgent NM-2)', function () {
+ beforeEach(async function () {
+ Object.assign(this, await loadFixture(fixture))
+
+ // Attacker holds CMTAT_DEPLOYER_ROLE so its re-entrant deployCMTAT reaches the guard (not onlyRole).
+ this.attackerContract = await ethers.deployContract('ReentrancyDeployAttacker', [])
+ // Malicious "logic": its fallback runs under the proxy's delegatecall and calls the attacker.
+ this.logicMock = await ethers.deployContract('ReentrantInitLogicMock', [
+ this.attackerContract.target
+ ])
+ // Counter-salt mode (useCustomSalt = false) — the mode NM-2 is about.
+ this.FACTORY = await ethers.deployContract('CMTAT_UUPS_FACTORY', [
+ this.logicMock.target,
+ this.admin,
+ false
+ ])
+
+ this.CMTATArg = [
+ this.admin.address,
+ ['CMTA Token', 'CMTAT', DEPLOYMENT_DECIMAL],
+ extraInformationAttributes,
+ [ZERO_ADDRESS]
+ ]
+
+ await this.FACTORY.connect(this.admin).grantRole(
+ CMTAT_DEPLOYER_ROLE,
+ this.attackerContract.target
+ )
+
+ // Pre-encode the nested deployCMTAT call the attacker will replay during construction.
+ this.reentrantCall = this.FACTORY.interface.encodeFunctionData('deployCMTAT', [
+ ethers.encodeBytes32String('reenter'),
+ this.CMTATArg
+ ])
+ })
+
+ it('testReentrantDeployIsBlockedByGuard', async function () {
+ // Arm the attacker: the initializer will re-enter deployCMTAT mid-deployment.
+ await this.attackerContract.configure(
+ this.FACTORY.target,
+ this.reentrantCall,
+ true
+ )
+
+ await expect(
+ this.FACTORY.connect(this.admin).deployCMTAT(
+ ethers.encodeBytes32String('outer'),
+ this.CMTATArg
+ )
+ ).to.be.reverted
+
+ // Nothing was registered: the whole deployment reverted.
+ expect(await this.FACTORY.cmtatCounterId()).to.equal(0)
+ expect(await this.FACTORY.CMTATProxyAddress(0)).to.equal(ZERO_ADDRESS)
+ })
+
+ it('testNonReentrantDeploySucceedsControl', async function () {
+ // Same malicious logic, but disarmed: the initializer no-ops, so the deployment completes.
+ // Proves the revert above is specifically the re-entry, not the mock plumbing or the role.
+ await this.attackerContract.configure(
+ this.FACTORY.target,
+ this.reentrantCall,
+ false
+ )
+
+ await expect(
+ this.FACTORY.connect(this.admin).deployCMTAT(
+ ethers.encodeBytes32String('outer'),
+ this.CMTATArg
+ )
+ ).to.emit(this.FACTORY, 'CMTATDeployed')
+
+ expect(await this.FACTORY.cmtatCounterId()).to.equal(1)
+ })
+})