Skip to content

Commit f1ae06d

Browse files
committed
refactor: unify token binding into shared TokenBindingModule (allowlist), drop TOKEN_CONTRACT_ROLE
1 parent a77a922 commit f1ae06d

9 files changed

Lines changed: 177 additions & 188 deletions

AGENTS.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ addressed by a `bytes32` name.
2626
- **Bound-token path** — the standard single-arg `IERC1643` functions
2727
(`setDocument(name,uri,hash)`, `removeDocument(name)`) let a bound token manage
2828
its **own** namespace (`_msgSender()`). Bind via the shared `ITokenBinding`
29-
surface: `bindToken(token)` / `unbindToken(token)` / `isTokenBound(token)`
30-
(uniform across both deployments). Role deployment binds over
31-
`TOKEN_CONTRACT_ROLE` (CMTA RuleEngine pattern); Ownable over an owner allowlist.
29+
surface: `bindToken(token)` / `unbindToken(token)` / `isTokenBound(token)`,
30+
implemented **once** for both deployments by `TokenBindingModule` — a single
31+
allowlist, NOT a role (there is no `TOKEN_CONTRACT_ROLE`). Binding is authorized
32+
by each deployment's document-management hook (DOCUMENT_MANAGER_ROLE / owner).
3233
NOTE: RuleEngine's `ERC3643ComplianceExtendedModule` is intentionally **not**
3334
reused for binding — it is an `IERC3643Compliance`, which would drag in
3435
transfer-compliance callbacks (`canTransfer`/`transferred`/`created`/`destroyed`)
@@ -47,10 +48,11 @@ addressed by a `bytes32` name.
4748
`hasRole` override).
4849
- **Flexible access control (CMTAT / RuleEngine pattern):** restricted functions
4950
use the `onlyDocumentManager` / `onlyBoundToken` modifiers, which delegate to
50-
overridable `internal virtual` hooks `_authorizeDocumentManagement()` /
51-
`_authorizeBoundTokenDocumentManagement()` (default `DOCUMENT_MANAGER_ROLE` /
52-
`TOKEN_CONTRACT_ROLE`). Keep the management implementation separate from the
53-
authorization logic — change *who* is authorized by overriding a hook, not by
51+
overridable `internal virtual` hooks `_authorizeDocumentManagement()` (per
52+
deployment: `DOCUMENT_MANAGER_ROLE` / owner) and
53+
`_authorizeBoundTokenDocumentManagement()` (implemented once by
54+
`TokenBindingModule` → allowlist check). Keep the management implementation
55+
separate from the authorization logic — change *who* is authorized via a hook, not by
5456
editing the management functions.
5557
- **CMTAT integration:** since CMTAT v3, a token uses the engine via CMTAT's
5658
`DocumentEngineModule` and `setDocumentEngine(engine)` (reads/writes are forwarded
@@ -65,11 +67,11 @@ src/
6567
│ # both management paths, batch functions, modifiers,
6668
│ # and the ABSTRACT _authorize* hooks (no access control)
6769
├── DocumentEngine.sol # Deployment #1: role-based access control
68-
│ # (AccessControlEnumerable, the role constants
69-
│ # DOCUMENT_MANAGER_ROLE / TOKEN_CONTRACT_ROLE, _authorize*
70-
│ # impls, hasRole), ERC-2771, supportsInterface, constructor
70+
│ # (AccessControlEnumerable, DOCUMENT_MANAGER_ROLE,
71+
│ # _authorizeDocumentManagement, hasRole), ERC-2771,
72+
│ # supportsInterface, constructor
7173
├── DocumentEngineOwnable.sol # Deployment #2: Ownable2Step (single owner) instead of
72-
│ # roles; owner-managed token binding (ITokenBinding)
74+
│ # roles; document mgmt + binding are owner-only
7375
├── DocumentEngineInvariant.sol # Shared errors only (incl. ERC1643InvalidName /
7476
│ # ERC1643MissingDocument); NO access-control specifics
7577
├── interfaces/
@@ -79,8 +81,10 @@ src/
7981
│ └── ITokenBinding.sol # Shared binding surface: bindToken / unbindToken /
8082
│ # isTokenBound + TokenBindingSet (both deployments)
8183
└── modules/
82-
└── VersionModule.sol # Version module: implements ERC-8303 version() + ERC-165,
83-
# holds the VERSION constant (currently "0.4.0")
84+
├── VersionModule.sol # Version module: implements ERC-8303 version() + ERC-165,
85+
│ # holds the VERSION constant (currently "0.4.0")
86+
└── TokenBindingModule.sol # Shared token-binding allowlist (ITokenBinding) + NotBoundToken;
87+
# wires the bound-token hook; used by both deployments
8488
8589
script/
8690
├── DeployDocumentEngine.s.sol # Deploy role-based DocumentEngine (env: DOCUMENT_ENGINE_ADMIN,

CHANGELOG.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,17 @@ Reference: [keepachangelog.com/en/1.1.0/](https://keepachangelog.com/en/1.1.0/)
5757

5858
### Added
5959

60-
- **Bound-token document management (RuleEngine binding pattern)**: implement the now-mandatory `IERC1643.setDocument(name, uri, hash)` and `removeDocument(name)`. They are gated by a new `TOKEN_CONTRACT_ROLE` and scoped to the caller (`_msgSender()`) own namespace. A token bound with `grantRole(TOKEN_CONTRACT_ROLE, token)` manages its own documents and can never affect another contract's documents. The existing admin overloads (explicit `address`, `DOCUMENT_MANAGER_ROLE`) are unchanged, so both systems work side by side. (RuleEngine's `ERC3643ComplianceExtendedModule` was evaluated for this but intentionally not reused — see the README.)
60+
- **Bound-token document management**: implement the now-mandatory `IERC1643.setDocument(name, uri, hash)` and `removeDocument(name)`, gated by the `onlyBoundToken` modifier and scoped to the caller (`_msgSender()`) own namespace. A token bound with `bindToken(token)` (see the shared binding module below) manages its own documents and can never affect another contract's documents. The admin overloads (explicit `address`, `DOCUMENT_MANAGER_ROLE`) are unchanged, so both systems work side by side. (RuleEngine's `ERC3643ComplianceExtendedModule` was evaluated for the binding but intentionally not reused — see the README.)
6161
- **Optional multi-token events**: alongside the standard `IERC1643` events, the engine now also emits `DocumentUpdatedForContract` / `DocumentRemovedForContract`, which carry the `smartContract` (token) address so off-chain indexers can tell which contract a document belongs to during multi-contract operations. See [`ERC-1643-proposition.md`](./doc/ERCSpecification/ERC-1643-proposition.md) for the proposed optional standard extension.
62-
- **Flexible access control (CMTAT / RuleEngine pattern)**: the restricted functions now use the `onlyDocumentManager` / `onlyBoundToken` modifiers, which delegate to overridable `internal virtual` authorization hooks `_authorizeDocumentManagement()` / `_authorizeBoundTokenDocumentManagement()` (default: `DOCUMENT_MANAGER_ROLE` / `TOKEN_CONTRACT_ROLE`). This separates the document-management implementation from the authorization logic, so a subclass can change *who* is authorized without touching the management functions. Default behavior is unchanged.
62+
- **Flexible access control (CMTAT / RuleEngine pattern)**: the restricted functions use the `onlyDocumentManager` / `onlyBoundToken` modifiers, which delegate to overridable `internal virtual` authorization hooks `_authorizeDocumentManagement()` / `_authorizeBoundTokenDocumentManagement()`. Each deployment implements the admin hook (`DOCUMENT_MANAGER_ROLE` or `owner`); the bound-token hook is implemented once by `TokenBindingModule` (the shared allowlist). This separates the document-management implementation from the authorization logic.
6363
- **Split into a base contract and a deployment contract** (CMTAT module/deployment pattern): the document-management logic and storage now live in the new abstract `DocumentEngineBase` (with abstract `_authorize*` hooks), while `DocumentEngine` is the deployment contract that defines the access control (`AccessControl`, the concrete hooks and `hasRole`) and the ERC-2771 wiring. The deployable `DocumentEngine` API and behavior are unchanged.
6464
- **Version module implementing ERC-8303**: the version is now exposed through a dedicated `VersionModule` (`src/modules/VersionModule.sol`) implementing the `IERC8303` interface (`src/interfaces/IERC8303.sol`). It adds a standard `version()` view function (in addition to the existing public `VERSION` constant) and advertises ERC-8303 via ERC-165 (`supportsInterface(0x54fd4d50) == true`). `DocumentEngine` combines the module's `supportsInterface` with the access-control base.
65-
- **Second deployment `DocumentEngineOwnable`** (`src/DocumentEngineOwnable.sol`): an alternative deployment that uses OpenZeppelin `Ownable2Step` (single owner, two-step transfer) instead of role-based access control, reusing the same `DocumentEngineBase` logic. Admin document management is restricted to the `owner`; the bound-token path uses an owner-managed binding allowlist (`setTokenBinding` / `isBoundToken`, the analog of `TOKEN_CONTRACT_ROLE`, reverting with `NotBoundToken`).
65+
- **Second deployment `DocumentEngineOwnable`** (`src/DocumentEngineOwnable.sol`): an alternative deployment that uses OpenZeppelin `Ownable2Step` (single owner, two-step transfer) instead of role-based access control, reusing the same `DocumentEngineBase` logic and the shared `TokenBindingModule`. Both document management and token binding are restricted to the `owner`.
6666

6767
### Changed (access control)
6868

6969
- `DocumentEngine` now inherits **`AccessControlEnumerable`** instead of `AccessControl`, adding on-chain enumeration of role members (`getRoleMember`, `getRoleMemberCount`) and advertising `IAccessControlEnumerable` via ERC-165. Default authorization behavior is unchanged.
70-
- Moved the role constants (`DOCUMENT_MANAGER_ROLE`, `TOKEN_CONTRACT_ROLE`) out of the shared `DocumentEngineInvariant` and into the role-based `DocumentEngine`, so `DocumentEngineInvariant` (and the `DocumentEngineOwnable` deployment) no longer carry access-control-specific constants. The invariant now holds only the shared errors and multi-token events.
70+
- Moved the `DOCUMENT_MANAGER_ROLE` constant out of the shared `DocumentEngineInvariant` and into the role-based `DocumentEngine`, so `DocumentEngineInvariant` (and the `DocumentEngineOwnable` deployment) no longer carry access-control-specific constants. The invariant now holds only the shared errors.
7171

7272
### Fixed (ERC-1643 conformance)
7373

@@ -80,7 +80,7 @@ Aligned the implementation with the updated [ERC-1643](./doc/ERCSpecification/er
8080

8181
### Added (token binding)
8282

83-
- **Shared `ITokenBinding` interface** (`src/interfaces/ITokenBinding.sol`): `bindToken(token)` / `unbindToken(token)` / `isTokenBound(token)` + `TokenBindingSet` event. Both deployments now implement it (and advertise `type(ITokenBinding).interfaceId` via ERC-165), so integrators bind/query a token the same way regardless of the access-control model. `DocumentEngine` implements it over `TOKEN_CONTRACT_ROLE` (grant/revoke/hasRole); `DocumentEngineOwnable` over its owner-managed allowlist (replacing the previous `setTokenBinding` / `isBoundToken`). The revert on an unbound write still differs per deployment (`AccessControlUnauthorizedAccount` vs `NotBoundToken`).
83+
- **Shared `ITokenBinding` interface + `TokenBindingModule`.** `bindToken(token)` / `unbindToken(token)` / `isTokenBound(token)` + `TokenBindingSet` event (`src/interfaces/ITokenBinding.sol`), implemented once for both deployments by `src/modules/TokenBindingModule.sol` — a single **allowlist**, not a role. Both deployments now share the exact same binding mechanism (same functions, event, and `NotBoundToken` revert on an unbound write) and advertise `type(ITokenBinding).interfaceId` via ERC-165. The role deployment **no longer uses `TOKEN_CONTRACT_ROLE`** (removed) — binding is authorized by the document-management hook (`DOCUMENT_MANAGER_ROLE`, or the `owner` in `DocumentEngineOwnable`).
8484

8585
### Notes / bottlenecks
8686

CLAUDE.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ addressed by a `bytes32` name.
2626
- **Bound-token path** — the standard single-arg `IERC1643` functions
2727
(`setDocument(name,uri,hash)`, `removeDocument(name)`) let a bound token manage
2828
its **own** namespace (`_msgSender()`). Bind via the shared `ITokenBinding`
29-
surface: `bindToken(token)` / `unbindToken(token)` / `isTokenBound(token)`
30-
(uniform across both deployments). Role deployment binds over
31-
`TOKEN_CONTRACT_ROLE` (CMTA RuleEngine pattern); Ownable over an owner allowlist.
29+
surface: `bindToken(token)` / `unbindToken(token)` / `isTokenBound(token)`,
30+
implemented **once** for both deployments by `TokenBindingModule` — a single
31+
allowlist, NOT a role (there is no `TOKEN_CONTRACT_ROLE`). Binding is authorized
32+
by each deployment's document-management hook (DOCUMENT_MANAGER_ROLE / owner).
3233
NOTE: RuleEngine's `ERC3643ComplianceExtendedModule` is intentionally **not**
3334
reused for binding — it is an `IERC3643Compliance`, which would drag in
3435
transfer-compliance callbacks (`canTransfer`/`transferred`/`created`/`destroyed`)
@@ -47,10 +48,11 @@ addressed by a `bytes32` name.
4748
`hasRole` override).
4849
- **Flexible access control (CMTAT / RuleEngine pattern):** restricted functions
4950
use the `onlyDocumentManager` / `onlyBoundToken` modifiers, which delegate to
50-
overridable `internal virtual` hooks `_authorizeDocumentManagement()` /
51-
`_authorizeBoundTokenDocumentManagement()` (default `DOCUMENT_MANAGER_ROLE` /
52-
`TOKEN_CONTRACT_ROLE`). Keep the management implementation separate from the
53-
authorization logic — change *who* is authorized by overriding a hook, not by
51+
overridable `internal virtual` hooks `_authorizeDocumentManagement()` (per
52+
deployment: `DOCUMENT_MANAGER_ROLE` / owner) and
53+
`_authorizeBoundTokenDocumentManagement()` (implemented once by
54+
`TokenBindingModule` → allowlist check). Keep the management implementation
55+
separate from the authorization logic — change *who* is authorized via a hook, not by
5456
editing the management functions.
5557
- **CMTAT integration:** since CMTAT v3, a token uses the engine via CMTAT's
5658
`DocumentEngineModule` and `setDocumentEngine(engine)` (reads/writes are forwarded
@@ -65,11 +67,11 @@ src/
6567
│ # both management paths, batch functions, modifiers,
6668
│ # and the ABSTRACT _authorize* hooks (no access control)
6769
├── DocumentEngine.sol # Deployment #1: role-based access control
68-
│ # (AccessControlEnumerable, the role constants
69-
│ # DOCUMENT_MANAGER_ROLE / TOKEN_CONTRACT_ROLE, _authorize*
70-
│ # impls, hasRole), ERC-2771, supportsInterface, constructor
70+
│ # (AccessControlEnumerable, DOCUMENT_MANAGER_ROLE,
71+
│ # _authorizeDocumentManagement, hasRole), ERC-2771,
72+
│ # supportsInterface, constructor
7173
├── DocumentEngineOwnable.sol # Deployment #2: Ownable2Step (single owner) instead of
72-
│ # roles; owner-managed token binding (ITokenBinding)
74+
│ # roles; document mgmt + binding are owner-only
7375
├── DocumentEngineInvariant.sol # Shared errors only (incl. ERC1643InvalidName /
7476
│ # ERC1643MissingDocument); NO access-control specifics
7577
├── interfaces/
@@ -79,8 +81,10 @@ src/
7981
│ └── ITokenBinding.sol # Shared binding surface: bindToken / unbindToken /
8082
│ # isTokenBound + TokenBindingSet (both deployments)
8183
└── modules/
82-
└── VersionModule.sol # Version module: implements ERC-8303 version() + ERC-165,
83-
# holds the VERSION constant (currently "0.4.0")
84+
├── VersionModule.sol # Version module: implements ERC-8303 version() + ERC-165,
85+
│ # holds the VERSION constant (currently "0.4.0")
86+
└── TokenBindingModule.sol # Shared token-binding allowlist (ITokenBinding) + NotBoundToken;
87+
# wires the bound-token hook; used by both deployments
8488
8589
script/
8690
├── DeployDocumentEngine.s.sol # Deploy role-based DocumentEngine (env: DOCUMENT_ENGINE_ADMIN,

0 commit comments

Comments
 (0)