Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -70,7 +70,7 @@ CMTATFactoryInvariant

CMTATFactoryRoot
|- AccessControl
|- VERSION = "0.3.0"
|- VERSION = "0.4.0"
|- cmtatsList / cmtatCounterId / CMTATProxyAddress(id)
|- useCustomSalt
|- _checkAndDetermineDeploymentSalt(...)
Expand Down
53 changes: 51 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -70,7 +70,7 @@ CMTATFactoryInvariant

CMTATFactoryRoot
|- AccessControl
|- VERSION = "0.3.0"
|- VERSION = "0.4.0"
|- cmtatsList / cmtatCounterId / CMTATProxyAddress(id)
|- useCustomSalt
|- _checkAndDetermineDeploymentSalt(...)
Expand Down
49 changes: 39 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
# 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**.
Each factory contract automates deployment using **deterministic addresses (via CREATE2)** and initializes CMTAT instances with a structured set of parameters passed in arguments by the deployer.

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)
Expand Down Expand Up @@ -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`**

Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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) | — |

Expand All @@ -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).
11 changes: 10 additions & 1 deletion contracts/libraries/CMTATFactoryRoot.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 ============ */
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion contracts/libraries/ContractVersion.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions contracts/light/CMTAT_LIGHT_BEACON_FACTORY.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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));
}

Expand All @@ -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,
Expand Down
Loading
Loading