Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1233b42
Update to CMTAT v3.3.0-rc1 and other related librares, implement ERC-…
rya-sge Jul 22, 2026
afb03c2
refactor: delegate access control to overridable _authorize hooks (CM…
rya-sge Jul 22, 2026
19cdb8f
refactor: split DocumentEngine into DocumentEngineBase (logic) + Docu…
rya-sge Jul 22, 2026
f544ce5
feat: add VersionModule implementing ERC-8303 (version() + ERC-165)
rya-sge Jul 22, 2026
183b648
feat: use AccessControlEnumerable and add DocumentEngineOwnable (Owna…
rya-sge Jul 22, 2026
1b9e49e
refactor: move role constants from DocumentEngineInvariant to Documen…
rya-sge Jul 22, 2026
a6a8a7c
revert: keep purpose-built token binding; document why RuleEngine's E…
rya-sge Jul 22, 2026
8fcd114
Add multi token support
rya-sge Jul 22, 2026
113a348
fix: conform DocumentEngine to updated ERC-1643 (emission responsibil…
rya-sge Jul 22, 2026
0bf09e9
feat: add Foundry deployment scripts for both DocumentEngine variants…
rya-sge Jul 22, 2026
2e3e114
Environment Variables
rya-sge Jul 22, 2026
34ec34d
refactor: unify document-manager parameter naming on subject/subjects
rya-sge Jul 22, 2026
a839562
feat: add shared ITokenBinding surface (bindToken/unbindToken/isToken…
rya-sge Jul 22, 2026
a77a922
docs: clarify base/invariant/hasRole NatSpec and mark DocumentEngine…
rya-sge Jul 22, 2026
f1ae06d
refactor: unify token binding into shared TokenBindingModule (allowli…
rya-sge Jul 22, 2026
ddc0881
style: adopt forge fmt as the canonical formatter (drop prettier), r…
rya-sge Jul 22, 2026
1432af9
perf: cache batch-loop length and use ++i in DocumentEngineBase
rya-sge Jul 22, 2026
5b96347
test: add event-emission, msg.sender-read, batch-guard, and fuzz cov…
rya-sge Jul 22, 2026
b26b74f
refactor: remove unused DocumentNotFound error
rya-sge Jul 22, 2026
39fd5c9
feat: reject null subject in multi-token setDocument (ERC1643InvalidS…
rya-sge Jul 22, 2026
5d13ee0
chore: upgrade to CMTAT v3.3.0-rc2, add version compatibility matrix…
rya-sge Jul 28, 2026
5bcde9d
refactor: move spec errors onto their interfaces, rename MultiDocumen…
rya-sge Jul 28, 2026
5059059
fix: move spec errors onto their interfaces, guard token binding, add…
rya-sge Jul 28, 2026
60a3d87
docs: regrade IMPROVEMENT item 1 — a global DOCUMENT_MANAGER_ROLE s…
rya-sge Jul 28, 2026
bbf23be
revert: keep advertising type(IERC1643).interfaceId — a token uses it…
rya-sge Jul 28, 2026
4f12880
chore: upgrade OpenZeppelin to v5.7.0, record CMTAT v3.3.0-rc3 / Rule…
rya-sge Aug 17, 2026
d3c81bb
fix: raise pragma to ^0.8.24 — the true src floor after CMTAT rc3 and…
rya-sge Aug 17, 2026
f6ddbe3
Update surya report
rya-sge Aug 17, 2026
6b8d7d5
docs: add first Slither run for v0.4.0, refresh Aderyn against CMTAT …
rya-sge Aug 17, 2026
68cec29
style: order functions per Solidity style guide, use named imports,…
rya-sge Aug 17, 2026
a2f2749
perf: cache storage pointers in document removal (-2845 gas), docume…
rya-sge Aug 17, 2026
0394e30
refactor: mark every internal function virtual (zero-gas, guarded by …
rya-sge Aug 17, 2026
3f600da
docs: drop doc/ pointers from contract comments, tightening each in p…
rya-sge Aug 17, 2026
793be81
docs: move CLAUDE_ANALYSIS.md under doc/audits/tools/v0.4.0/claude/
rya-sge Aug 17, 2026
bb374d7
docs: regenerate surya and re-run aderyn/slither after the code-qual…
rya-sge Aug 17, 2026
0ae9a27
chore: regenerate coverage for v0.4.0 and correct the release checkli…
rya-sge Aug 17, 2026
a65e637
docs: retire IMPROVEMENT.md into AUDIT_OVERVIEW open items, add CMTAT…
rya-sge Aug 17, 2026
77b9bd3
docs: split README into a short entry point and doc/DOCUMENTATION.md
rya-sge Aug 17, 2026
5698bf2
docs: add SECURITY.md and fix the broken security-policy links in CON…
rya-sge Aug 17, 2026
2f9a44f
docs: add simplified write/read diagrams to the README, keep the det…
rya-sge Aug 17, 2026
9011ab4
docs: split the architecture schema into topology + contract structu…
rya-sge Aug 17, 2026
31e19e2
docs: rewrap CHANGELOG and both READMEs to a consistent 100 columns
rya-sge Aug 17, 2026
995f725
docs: unwrap prose to one line per block in CHANGELOG and both README…
rya-sge Aug 17, 2026
899ff3f
Run npm audit fix
rya-sge Aug 17, 2026
523746d
docs: add a table of contents to doc/README.md and fix the orphaned…
rya-sge Aug 17, 2026
3059d0e
docs: state the verified CMTAT compatibility range (rc2–rc3) instead …
rya-sge Aug 17, 2026
155a7fe
Update doc
rya-sge Aug 17, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ docs/

# Dotenv file
.env

history
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
[submodule "lib/openzeppelin-contracts-upgradeable"]
path = lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
[submodule "lib/RuleEngine"]
path = lib/RuleEngine
url = https://github.com/CMTA/RuleEngine
220 changes: 220 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# DocumentEngine — Agent Guide

> **Note — keep in sync:** `AGENTS.md` and `CLAUDE.md` must always be **identical**.
> Any edit to one must be applied verbatim to the other.

> **Note — commit messages:** After each group of modifications or each feature
> added, always provide a **one-line GitHub commit message** (Conventional-Commits
> style, e.g. `feat: add token binding`, `fix: correct event args`, `docs: update README`).

## What this project is

`DocumentEngine` is a standalone smart contract that manages documents on-chain
through **ERC-1643** on behalf of **several** other smart contracts (e.g. CMTAT
tokens). Using an external engine keeps each token small and lets one operator
manage documents for a whole fleet of tokens.

A document is `{ string uri, bytes32 documentHash, uint256 lastModified }`,
addressed by a `bytes32` name.

## Key concepts

- **Two management paths (both active at once):**
- **Admin path** — `DOCUMENT_MANAGER_ROLE`. Address-scoped overloads
(`setDocument(address,...)`, `removeDocument(address,...)`, batch variants)
manage documents for any contract.
- **Bound-token path** — the standard single-arg `IERC1643` functions
(`setDocument(name,uri,hash)`, `removeDocument(name)`) let a bound token manage
its **own** namespace (`_msgSender()`). Bind via the shared `ITokenBinding`
surface: `bindToken(token)` / `unbindToken(token)` / `isTokenBound(token)`,
implemented **once** for both deployments by `TokenBindingModule` — a single
allowlist, NOT a role (there is no `TOKEN_CONTRACT_ROLE`). Binding is authorized
by each deployment's document-management hook (DOCUMENT_MANAGER_ROLE / owner).
NOTE: RuleEngine's `ERC3643ComplianceExtendedModule` is intentionally **not**
reused for binding — it is an `IERC3643Compliance`, which would drag in
transfer-compliance callbacks (`canTransfer`/`transferred`/`created`/`destroyed`)
irrelevant to a document engine. See the README rationale section.
- **Events (ERC-1643 emission responsibility):** this engine is a *shared,
multi-token* manager, so it emits **only** the address-carrying extension events
`DocumentUpdatedForSubject` / `DocumentRemovedForSubject` (param `subject`) and
**not** the base `DocumentUpdated` / `DocumentRemoved` (those carry no address and
are the token contract's responsibility). Extension declared in
`src/interfaces/IERC1643MultiDocument.sol`; rationale in `ERC-1643-proposition.md`.
- **Errors live on interfaces, not on `DocumentEngineInvariant`.** Each specification
error is declared by the interface defining its condition — `ERC1643InvalidName` /
`ERC1643MissingDocument` on `IERC1643`, `MultiDocumentInvalidSubject` on
`IERC1643MultiDocument`, `TokenBindingInvalidToken` on `ITokenBinding` — so an ABI
generated from an interface carries its errors and each is obtained exactly once
(the multi-subject draft's "MUST NOT declare them twice", also a compile error).
`DocumentEngineInvariant` keeps only errors no interface defines.
- **Token binding is idempotent and rejects `address(0)`:** `bindToken` / `unbindToken`
write and emit `TokenBindingSet` only on an actual change, so every event is a real
transition; a repeat call succeeds silently. `address(0)` reverts
`TokenBindingInvalidToken()`.
- **ERC-1643 conformance:** `setDocument` reverts `ERC1643InvalidName()` on
`name == 0`; `removeDocument` reverts `ERC1643MissingDocument()` on a missing doc;
`supportsInterface` advertises `IERC1643` + `IERC1643MultiDocument` + `ITokenBinding`
(both deployments). The base id is for a **token** checking that the single-argument
endpoints exist before wiring itself to the engine — it does **not** mean documents
should be read from the engine's address, since those functions are `_msgSender()`-scoped.
Both errors are declared by `IERC1643` itself since CMTAT `v3.3.0-rc2` (still true in
`v3.3.0-rc3`) — do **not**
re-declare them in `DocumentEngineInvariant` (duplicate declaration = compile error,
and the multi-subject draft forbids it).
- **`getDocument` returns flat values**, `(string uri, bytes32 documentHash,
uint256 lastModified)`, never the `Document` struct — the struct is storage-only.
A struct return prepends an offset word to the returndata while leaving the selector
and `type(IERC1643).interfaceId` unchanged, so the mismatch is invisible to ERC-165
and a spec-conformant consumer silently mis-decodes. Pinned by
`testGetDocumentReturnsFlatErc1643Abi`.
- **ERC-2771:** meta-transaction (gasless) support; `_msgSender()` is used everywhere.
- **Access control:** `DEFAULT_ADMIN_ROLE` implicitly has every role (see the
`hasRole` override).
- **No documentation pointers in contract comments.** Never write `See doc/…` or a `.md` path in
`src/` — docs move, deployed source does not, and a reader on a block explorer has neither. State
the conclusion in the comment instead, and keep it short; the derivation belongs in `doc/` with no
cross-reference either way. NatSpec links that resolve inside the source (`{_removeDocument}`) are
fine. Exempt: tests/mocks, and citations of audit records by **bare filename + finding ID**
(`CLAUDE_ANALYSIS.md (H-1)`) — those are immutable and survive a move.
- **Every `internal` function is `virtual`.** Not just the `_authorize*` hooks — the document
write/read paths (`_setDocument`, `_removeDocument`, `_removeDocumentName`, `_getDocument`), the
binding internals (`_setTokenBinding`, `_checkTokenBound`) and the ERC-2771 context trio are all
overridable. It costs nothing at runtime (bytecode is byte-identical with and without the keyword),
so **keep new internal functions `virtual`**; `OverridingDocumentEngine` in the test suite fails to
compile if one loses it.
- **Flexible access control (CMTAT / RuleEngine pattern):** restricted functions
use the `onlyDocumentManager` / `onlyBoundToken` modifiers, which delegate to
overridable `internal virtual` hooks `_authorizeDocumentManagement()` (per
deployment: `DOCUMENT_MANAGER_ROLE` / owner) and
`_authorizeBoundTokenDocumentManagement()` (implemented once by
`TokenBindingModule` → allowlist check). Keep the management implementation
separate from the authorization logic — change *who* is authorized via a hook, not by
editing the management functions.
- **CMTAT integration:** since CMTAT v3, a token uses the engine via CMTAT's
`DocumentEngineModule` and `setDocumentEngine(engine)` (reads/writes are forwarded
keyed by the token address). Standard CMTAT standalone tokens store documents
on-chain instead and do **not** use this engine.

## File tree

```
src/
├── DocumentEngineBase.sol # Abstract base: ERC-1643 document logic + storage,
│ # both management paths, batch functions, modifiers,
│ # and the ABSTRACT _authorize* hooks (no access control)
├── DocumentEngine.sol # Deployment #1: role-based access control
│ # (AccessControlEnumerable, DOCUMENT_MANAGER_ROLE,
│ # _authorizeDocumentManagement, hasRole), ERC-2771,
│ # supportsInterface, constructor
├── DocumentEngineOwnable.sol # Deployment #2: Ownable2Step (single owner) instead of
│ # roles; document mgmt + binding are owner-only
├── DocumentEngineInvariant.sol # Non-specification errors ONLY (InvalidInputLength,
│ # AdminWithAddressZeroNotAllowed). Every spec error is
│ # declared by its own interface — see the note below.
│ # NO access-control specifics
├── interfaces/
│ ├── IERC8303.sol # ERC-8303 "Contract Version" interface (id 0x54fd4d50)
│ ├── IERC1643MultiDocument.sol # Multi-token ERC-1643 extension (address-scoped fns +
│ │ # DocumentUpdatedForSubject / DocumentRemovedForSubject +
│ │ # MultiDocumentInvalidSubject)
│ └── ITokenBinding.sol # Shared binding surface: bindToken / unbindToken /
│ # isTokenBound + TokenBindingSet + TokenBindingInvalidToken
└── modules/
├── VersionModule.sol # Version module: implements ERC-8303 version() + ERC-165,
│ # holds the VERSION constant (currently "0.4.0")
└── TokenBindingModule.sol # Shared token-binding allowlist (ITokenBinding) + NotBoundToken;
# wires the bound-token hook; used by both deployments

script/
├── DeployDocumentEngine.s.sol # Deploy role-based DocumentEngine (env: DOCUMENT_ENGINE_ADMIN,
│ # DOCUMENT_ENGINE_FORWARDER); run()=env, deploy(admin,fwd)=testable
└── DeployDocumentEngineOwnable.s.sol # Deploy Ownable variant (env: DOCUMENT_ENGINE_OWNER, _FORWARDER)

test/
├── DocumentEngine.t.sol # Foundry tests: deploy, access control, admin + bound-token
│ # paths, batch ops (incl. name==0 / missing-doc guards),
│ # ERC-8303 + interface discovery, event emission (asserts the
│ # base events are NOT emitted), msg.sender-scoped reads,
│ # enumeration, fuzz round-trip/isolation, CMTAT integration
│ # (CMTATDocumentEngineMock), flexible-auth override (OpenDocumentEngine)
├── DocumentEngineOwnable.t.sol # Tests for the Ownable2Step deployment (owner path,
│ # token binding, two-step ownership, ERC-8303)
└── Deploy.t.sol # Tests for the deployment scripts (deploy() state + run() env)
```

**Contract split (CMTAT module/deployment pattern):** `DocumentEngineBase` holds
the document logic and abstract `_authorize*` hooks; each deployment contract
supplies the concrete access control. There are two deployments —
`DocumentEngine` (role-based, `AccessControlEnumerable`) and
`DocumentEngineOwnable` (`Ownable2Step`). Add new management logic in the base;
change *who* is authorized in a deployment (implement the `_authorize*` hooks).

Other important files:

- `foundry.toml` — solc `0.8.34`, `evm_version = prague` (required by CMTAT v3).
Sources declare `pragma solidity ^0.8.24` — the real `src/` floor, set by OpenZeppelin's
`AccessControlEnumerable`/`EnumerableSet` and CMTAT's `draft-IERC1643` since `v3.3.0-rc3`.
Building the tests needs `≥ 0.8.27` (CMTAT's `require(cond, CustomError())` is via-ir-only
before then). Keep the pragma honest: if a dependency raises its floor, raise ours to match.
- `remappings.txt` — `CMTAT/`, `RuleEngine/`, `OZ/`, `@openzeppelin/contracts-upgradeable/`.
- `CHANGELOG.md` — semver history; update on every release (current: `v0.4.0`).
- `ERC-1643-proposition.md` — proposed optional multi-token events / extension.
- `README.md` — **short** entry point only: what the engine is, quick start, the two management
paths, the CMTAT wiring, the two integrator caveats, deploy. Keep it short; new prose belongs in
the full document.
- `doc/README.md` — the specification / full reference (Surya schema, ERC-165 rationale, version
compatibility matrix, tooling). This is where the old root README moved.
- `doc/img/` — PlantUML **sources** (`*.puml`) plus their rendered `*.png`. Five diagrams, split by
audience: `cmtat-write-simple` / `cmtat-read-simple` are the **short** pair, used in *both*
`README.md` and `doc/README.md`; `cmtat-integration-architecture` (topology),
`documentengine-contract-structure` (inheritance) and `cmtat-integration-sequence` (full call
flow with every revert branch) belong to `doc/README.md` only — keep them out of the root README.
One diagram, one job: when a schema needs a legend to stay legible, split it instead. Only images
are embedded, never the source. Re-render with `plantuml -tpng doc/img/<name>.puml`, and look
at the PNG: PlantUML draws syntax/deprecation warnings *into* the image and still exits 0.
- `doc/` — Surya output in `doc/surya/{surya_graph,surya_inheritance,surya_report}`, one file per
`.sol` in `src/` (9 each), regenerated by the three scripts in `doc/script/` — run them from that
directory, **graph first** (it creates the scratch `docOut/`; the report script's `mkdir` lacks
`-p`). Patch `surya/lib/graph.js` before regenerating or every contract calling `super.<fn>()`
yields a silent 0-byte PNG; see the Surya section in `doc/README.md`. Also coverage, and
`doc/audits/` — the security overview (`AUDIT_OVERVIEW.md`) plus versioned
static-analysis output under `doc/audits/tools/vX.Y.Z/<tool>/`, each with a
`*-report.md` (summary table prepended) and a `*-report-feedback.md` triaging
every finding, plus a `claude/` section holding the AI-assisted code-quality review.
Both Aderyn `0.6.5` (0 High · 6 Low) and Slither `0.11.5`
(0 High · 0 Med · 0 Low · 2 Info) were run for `v0.4.0` — nothing to fix in either.
Slither's dependency filter must be `lib` (Foundry layout); `--filter-paths` fails
open, so an entry matching nothing silently pulls the vendored tree into scope.
`doc/audits/tools/v0.4.0/claude/CLAUDE_ANALYSIS.md` is the code-quality review (not a security audit) — read
its "left as is" rows before proposing an optimisation: `unchecked {++i}` (0 gas on solc
0.8.34), `string calldata` on the admin `setDocument` (49 gas *worse*), and extracting the
duplicated ERC-2771 overrides (impossible — C3 linearization) are all measured dead ends.
- **Open items live in `doc/audits/AUDIT_OVERVIEW.md`** under *Known open items*, with stable
`OPEN-n` ids that the audit reports cite. There is no `IMPROVEMENT.md` — it was folded in.
Update that table when an item is fixed (move the record to `CHANGELOG.md`) or when review
surfaces a new one; keep the existing ids stable so the citations stay valid.
- `lib/` — submodules: `CMTAT`, `RuleEngine`, `openzeppelin-contracts(-upgradeable)`, `forge-std`.

## Dependencies (tested versions)

- CMTAT `v3.3.0-rc3`, RuleEngine `v3.0.0-rc5` (binding-pattern reference only; compliance module not reused)
- OpenZeppelin Contracts / Contracts Upgradeable `v5.7.0`
- Solidity `0.8.34`, Foundry

## Common commands

```bash
forge build # compile
forge test # run the test suite
forge fmt # format
forge test --gas-report
```

## Conventions

- The `VERSION` constant (in `src/modules/VersionModule.sol`, exposed via
ERC-8303 `version()`) must match the latest `CHANGELOG.md` entry on release.
- Bump `MAJOR` on incompatible proxy-storage / external-library or API changes,
`MINOR` for backward-compatible features, `PATCH` for backward-compatible fixes.
- A bound token can only ever affect its **own** document namespace — never break
that isolation.
Loading
Loading