Skip to content
Open

Dev #13

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2c8a6f3
feat(rules): add multi-token ConditionalTransferLight + tests/docs, w…
rya-sge May 11, 2026
f49f0b9
Update version to v0.4.0
rya-sge May 11, 2026
3cbe2ca
refactor(erc165): extract Ownable2StepERC165Module, apply to all Owna…
rya-sge May 11, 2026
c95018f
fix(cmtat): upgrade to v3.3.0-rc1, rename CMTATStandalone→CMTATStanda…
rya-sge Jun 8, 2026
58ac60e
feat(rules): add RuleMintAllowance with single-target binding, tests/…
rya-sge Jun 10, 2026
a7fb7dd
test(rules): cover RuleMintAllowance batch mint and advertised interf…
rya-sge Jun 10, 2026
9b9221a
fix(rules): stop advertising ERC3643 compliance for mint allowance
rya-sge Jun 10, 2026
f5d5051
chore(deps): upgrade RuleEngine to v3.0.0-rc4 and inherit isolated ro…
rya-sge Jul 6, 2026
5ae92cd
update changelog
rya-sge Jul 6, 2026
7e6cd40
Update surya schema
rya-sge Jul 6, 2026
699a3b1
Add per-rule CMTAT transfer-flow
rya-sge Jul 6, 2026
e24b2dc
docs(readme): fix RuleEngine schema link and align version info (Rule…
rya-sge Jul 8, 2026
001b5aa
docs(readme): add integration-modes overview, real TOC, list RuleMint…
rya-sge Jul 8, 2026
6a8fa44
docs(readme): add Which rule should I use? selection guide and Bindi…
rya-sge Jul 8, 2026
d34a649
docs(readme): fix typos, align restriction-code table, qualify CMTA…
rya-sge Jul 8, 2026
b63fe1f
docs(readme): clarify ERC-3643 summary column and add RuleMintAllow…
rya-sge Jul 8, 2026
725c4bf
docs(readme): lift Overview intro above Schema and move Schema below…
rya-sge Jul 8, 2026
44916a1
docs(readme): move Quick Start to sit directly before the Deploymen…
rya-sge Jul 8, 2026
739b46d
Apply solidity style guideline to src production contracts
rya-sge Jul 8, 2026
5fb5e40
style: apply function ordering and /** */ NatSpec to src/mocks (beha…
rya-sge Jul 8, 2026
e2dbc21
fix(mocks): correct SanctionListOracle.removeFromSanctionsList to un-…
rya-sge Jul 8, 2026
4ff0f26
Add plantuml schema
rya-sge Jul 8, 2026
0addfbc
Add audit tools with slither and aderyn
rya-sge Jul 9, 2026
fcddd84
refactor(RuleERC2980): split shared list errors into per-list errors …
rya-sge Jul 10, 2026
6611d54
fix(RuleMaxTotalSupply): overflow-safe restriction views (return code…
rya-sge Jul 10, 2026
01ad1ac
docs(RuleMintAllowance): flag canTransfer as non-authoritative; docu…
rya-sge Jul 10, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ docOut/
node_modules/
/history
/ERCSpecification
FEEDBACK.md
*.dbg.json

# Codex
Expand All @@ -27,3 +28,5 @@ node_modules/
#drawio
*.bkp
*.dtmp


67 changes: 59 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,54 @@
# Project Guide

## Purpose
Modular compliance-rule library for CMTAT / ERC-3643 security tokens. Each rule enforces a transfer restriction (whitelist, spender whitelist, blacklist, sanctions, max supply, identity, conditional approval) and can be used standalone or composed via a `RuleEngine`.
Modular compliance-rule library for CMTAT / ERC-3643 security tokens. Each rule enforces a transfer restriction (whitelist, spender whitelist, blacklist, sanctions, max supply, identity, conditional approval, mint quota) and can be used standalone or composed via a `RuleEngine`.

## Architecture Overview
A rule answers two questions about a proposed token movement:
- **Read path** — `detectTransferRestriction` / `detectTransferRestrictionFrom` / `canTransfer` / `canTransferFrom` return an ERC-1404 restriction code (`0` = OK). These are views and must not revert.
- **Write path** — `transferred` / `created` / `destroyed` are called by the token *after* it has decided to move value; the rule **reverts** to block, and may mutate state.

### The two integration topologies (this determines `msg.sender` inside a rule)
```
Topology A — "RuleEngine mode" (default)
CMTAT ──transferred(spender,from,to,value)──▶ RuleEngine ──transferred(...)──▶ Rule
msg.sender == RuleEngine
⇒ bind the RuleEngine to an operation rule, not the token.

Topology B — "direct mode" (CMTAT.setRuleEngine(rule))
CMTAT ──transferred(spender,from,to,value)──▶ Rule
msg.sender == CMTAT token
⇒ bind the token.
```
Operation rules that treat `msg.sender` or `getTokenBound()` as a *token identity* behave differently in the two topologies. `approveAndTransferIfAllowed` only works in Topology B.

### The mint/burn `spender` convention (CMTAT v3.3+)
`CMTAT._mintOverride` calls `_checkTransferred(_msgSender(), address(0), to, value)`, so **on every mint the minter's address arrives at each rule as `spender`** via the 4-arg `transferred` overload. Plain `transfer()` passes `spender == address(0)` and takes the 3-arg path.

- `RuleWhitelist`, `RuleSpenderWhitelist`, `RuleWhitelistWrapper` explicitly exempt mint/burn from the spender check.
- `RuleIdentityRegistry`, `RuleBlacklist`, `RuleSanctionsList`, `RuleERC2980` do **not** — they screen the minter. For the deny-lists this is intended; for `RuleIdentityRegistry` it means the minter must itself be identity-verified (see `RESULT.md` F-1).
- `RuleMintAllowance` is the only rule that *uses* the mint spender: it debits `mintAllowance[spender]`.

## Key Directories
| Path | Description |
|---|---|
| `src/rules/validation/` | Read-only rules (view functions, no state changes during transfer) |
| `src/rules/operation/` | Read-write rules (modify state on transfer) |
| `src/rules/validation/abstract/core/` | `RuleTransferValidation` (ERC-1404/3643/7551 views), `RuleNFTAdapter` (ERC-7943 + `ITransferContext` overloads), `RuleWhitelistShared` |
| `src/rules/validation/abstract/` | Shared base contracts and invariant storage |
| `src/rules/interfaces/` | Shared interfaces (`IAddressList`, `IIdentityRegistry`, `ISanctionsList`) |
| `src/modules/` | Reusable modules (`AccessControlModuleStandalone`, `MetaTxModuleStandalone`, `VersionModule`) |
| `src/rules/interfaces/` | Shared interfaces (`IAddressList`, `IIdentityRegistry`, `ISanctionsList`, `ITotalSupply`, `ITransferContext`, `IERC2980`, `IERC7943NonFungibleCompliance`) |
| `src/modules/` | Reusable modules (`AccessControlModuleStandalone`, `MetaTxModuleStandalone`, `VersionModule`, `Ownable2StepERC165Module`) |
| `test/` | Foundry tests, one folder per rule |
| `lib/` | Git submodule dependencies (do not edit) |

## Key Files to Read First
1. `src/rules/validation/abstract/core/RuleTransferValidation.sol` — the read-path interface every rule implements; declares the `_detectTransferRestriction*` hooks.
2. `src/rules/validation/abstract/core/RuleNFTAdapter.sol` — adds the ERC-7943 `tokenId` overloads and the `ITransferContext` struct entrypoints; declares the `_transferred*` write hooks.
3. `src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol` — the `EnumerableSet` membership machinery shared by whitelist/blacklist/spender-whitelist.
4. `src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol` — the approval state machine (`approvalCounts`, `_transferHash`).
5. `src/rules/operation/abstract/RuleMintAllowanceBase.sol` — the only rule keyed on the mint `spender`.
6. `lib/RuleEngine/src/RuleEngineBase.sol` — how a rule actually gets called.

## Main Contracts
| Contract | Role |
|---|---|
Expand All @@ -28,15 +63,18 @@ Modular compliance-rule library for CMTAT / ERC-3643 security tokens. Each rule
| `RuleERC2980Ownable2Step` | Ownable2Step variant of RuleERC2980 |
| `RuleConditionalTransferLight` | Require operator approval before each transfer; bound to exactly one token at a time (`bindToken` reverts if a token is already bound; use `unbindToken` first to migrate) |
| `RuleConditionalTransferLightOwnable2Step` | Owner-only approval and execution for conditional transfers |
| `RuleConditionalTransferLightMultiToken` / `…Ownable2Step` | Conditional transfers with approvals keyed `(token, from, to, value)`. **Caveat**: approvals are *consumed* under `msg.sender`, so per-token scoping only holds in Topology B (direct binding). See `RESULT.md` F-4 |
| `RuleMintAllowance` / `RuleMintAllowanceOwnable2Step` | Per-minter mint quota, debited on the 4-arg `transferred(spender, from=0, to, value)` path. Requires CMTAT ≥ v3.3. `canTransfer` is **not** authoritative for this rule — use `canTransferFrom(minter, address(0), to, value)` |
| `AccessControlModuleStandalone` | Base RBAC module; admin implicitly holds all roles |
| `MetaTxModuleStandalone` | ERC-2771 meta-transaction support |
| `MetaTxModuleStandalone` | ERC-2771 meta-transaction support. Note: the operation rules deliberately do **not** inherit this, so `_msgSender()` used as a binding identity is never forwarder-controlled |
| `Ownable2StepERC165Module` | Shared ERC-165 advertisement (`IERC173`, `IOwnable2Step`) for the Ownable2Step variants |
| `VersionModule` | Implements `IERC3643Version`; returns the contract version string |

## Dependencies (lib/)
- `openzeppelin-contracts` v5.6.1 — `AccessControl`, `Ownable2Step`, `EnumerableSet`, `ERC2771Context`
- `openzeppelin-contracts-upgradeable` v5.6.1
- `CMTAT` v3.0.0 — `IERC1404`, `IERC3643`, `IRuleEngine` interfaces
- `RuleEngine` v3.0.0-rc2 — `IRule`, `RulesManagementModule`
- `RuleEngine` v3.0.0-rc4 — `IRule`, `RulesManagementModule`
- `forge-std` — Foundry test utilities

Remappings are in `remappings.txt`; aliases used in source: `OZ/`, `CMTAT/`, `RuleEngine/`.
Expand All @@ -52,21 +90,22 @@ Foundry config: `foundry.toml` (solc 0.8.34, EVM prague, optimizer 200 runs).
## Restriction Code Ranges
| Rule | Codes |
|---|---|
| RuleWhitelist | 21–23 |
| RuleWhitelist / RuleWhitelistWrapper | 21–23 |
| RuleSanctionsList | 30–32 |
| RuleBlacklist | 36–38 |
| RuleConditionalTransferLight | 46 |
| RuleConditionalTransferLight / …MultiToken | 46 |
| RuleMaxTotalSupply | 50 |
| RuleIdentityRegistry | 55–57 |
| RuleERC2980 | 60–63 |
| RuleSpenderWhitelist | 66 |
| RuleMintAllowance | 70 |

## Conventions
- Each rule has an `InvariantStorage` abstract contract holding its constants, custom errors, and events.
- Access control is implemented via an abstract `_authorize*()` method overridden by concrete subclasses (template method pattern).
- AccessControl variants must use `onlyRole(ROLE)` in `_authorize*()` methods (avoid direct `_checkRole`).
- AccessControl variants treat the default admin as having all roles via `hasRole`, but the admin may not appear in role member enumerations unless explicitly granted.
- All rules implement `IERC3643Version` via `VersionModule`; the current version string is `"0.2.0"`.
- All rules implement `IERC3643Version` via `VersionModule`; the current version string is `"0.4.0"` (asserted by `test/Version.t.sol`).
- **ERC-165 interface IDs**: `type(IFoo).interfaceId` only XORs selectors defined directly on `IFoo` and does NOT include selectors from inherited interfaces. Always use the pre-computed library constants instead: `ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID` (from `CMTAT/library/`), `RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID` (from `CMTAT/library/`), and `RuleInterfaceId.IRULE_INTERFACE_ID` (from `RuleEngine/modules/library/`). If no pre-computed constant exists for an interface, define a flat mock interface that redeclares all functions from the full inheritance tree and use `type(IFooFlattened).interfaceId` to compute the correct value (see `lib/RuleEngine/src/mocks/IRuleInterfaceIdHelper.sol` for the established pattern).
- Batch add/remove operations are non-reverting (skip duplicates); single-item operations revert on invalid input.
- All `internal` functions should be marked `virtual`.
Expand All @@ -76,3 +115,15 @@ Foundry config: `foundry.toml` (solc 0.8.34, EVM prague, optimizer 200 runs).
- `AGENTS.md` and `CLAUDE.md` are identical — always update both together.
- Always update README.md with the latest change
- New rule or features implemented: create/update technical documentation in `doc/technical`, update README, create/update test (target: 100% of code coverage), update CHANGELOG.md. Code coverage, run `forge coverage --report summary`
- After each implemented feature or fix, provide a one-line GitHub commit message for all changes since the last commit.

## Security Findings Reference
- [`THREAT_MODEL.md`](THREAT_MODEL.md) — trust model, 30 catalogued threats with IDs, data-flow diagrams, 12 invariants.
- [`RESULT.md`](RESULT.md) — findings (0 High/Medium, 2 Low, 8 Info), invariant and access-control verification, disposition of every threat ID.
- [`TEST_IMPROVEMENT.md`](TEST_IMPROVEMENT.md) — test-gap analysis and the deferred test backlog.
- [`test/ThreatModel/ThreatModelTests.t.sol`](test/ThreatModel/ThreatModelTests.t.sol) — 18 PoCs. Tests suffixed `_CurrentBehaviour` assert behaviour the audit considers wrong; **fixing the underlying issue must make them fail**, at which point update the test and the finding together.

Gotchas worth knowing before you change anything:
- `HelperContract` already inherits `RuleConditionalTransferLightInvariantStorage`; inheriting the multi-token variant alongside it is a compile error (`OPERATOR_ROLE`, `CODE_TRANSFER_REQUEST_NOT_APPROVED` clash).
- `RuleWhitelistWrapperBase._detectTransferRestrictionForTargets` short-circuits once every target address is resolved, so a broken child rule may never be reached for some address pairs.
- `RuleWhitelistWrapper` does not ERC-165-check its child rules (unlike `RuleEngineBase._checkRule`); a non-`IAddressList` child bricks the scan.
61 changes: 60 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,66 @@ Custom changelog tag: `Dependencies`, `Documentation`, `Testing`
- Update surya doc by running the 3 scripts in [./doc/script](./doc/script)
- Update changelog

## v0.3.0 -


## Unreleased

### Fixed

- `RuleMaxTotalSupply`: `detectTransferRestriction` / `canTransfer` / `detectTransferRestrictionFrom` no longer revert with an arithmetic panic when `currentSupply + value` would overflow `uint256`. The mint check now compares against the remaining headroom (`value > maxTotalSupply - currentSupply`), so these ERC-1404 / ERC-3643 views always return a restriction code as required. Enforcement (`transferred`) is unchanged.

### Changed

- `RuleERC2980`: split the shared list-management errors into per-list errors so a revert identifies which list rejected. `RuleERC2980_AddressAlreadyListed` becomes `RuleERC2980_AddressAlreadyWhitelisted` / `RuleERC2980_AddressAlreadyFrozen`, and `RuleERC2980_AddressNotFound` becomes `RuleERC2980_AddressNotWhitelisted` / `RuleERC2980_AddressNotFrozen`. **Breaking (ABI):** the removed errors' 4-byte selectors no longer exist; off-chain tooling matching on them must update.

### Testing

- Add `test/ThreatModel/ThreatModelTests.t.sol` — 18 threat-model proof-of-concept tests (15 unit/integration, 3 fuzz) covering the identity-registry mint path, `RuleMaxTotalSupply` view overflow, the multi-token approval-key divergence, `approveAndTransferIfAllowed` under a `RuleEngine`, residual state after `unbindToken`, `_transferHash` injectivity, mint-quota accounting, and `RuleWhitelistWrapper` child-rule composition. Full suite: 425 tests, production line coverage 94.91%.

### Documentation

- `RuleMintAllowance`: document that `canTransfer` / `detectTransferRestriction` are **not authoritative** (hardcoded to "allowed" because the 3-arg path has no minter identity) and that a mint pre-flight must use the spender-aware `canTransferFrom(minter, address(0), to, value)` / `detectTransferRestrictionFrom`. Added a bold callout and an eligibility-views table to `doc/technical/RuleMintAllowance.md` and a warning to the README rule section.
- Add `THREAT_MODEL.md`, `RESULT.md` and `TEST_IMPROVEMENT.md` — manual security review of `src/` (0 High/Medium, 2 Low, 8 Info). Slither call-graph / inheritance / function-summary artifacts in `AUDIT/slither-graph/`.
- Add a "Manual Threat Model & Review" section to `README.md`.
- `CLAUDE.md` / `AGENTS.md`: correct the version string to `0.4.0`, document the two integration topologies and the CMTAT v3.3+ mint `spender` convention, and add the missing `RuleMintAllowance`, `RuleConditionalTransferLightMultiToken`, `RuleNFTAdapter` and restriction code `70` entries.

## v0.4.0

### Added

- `RuleConditionalTransferLightMultiToken` and `RuleConditionalTransferLightMultiTokenOwnable2Step` — multi-token conditional transfer rules with token-scoped approvals keyed by `(token, from, to, value)`.
- `RuleMintAllowance` and `RuleMintAllowanceOwnable2Step` — operation rule enforcing a per-minter mint quota managed by an operator. Each mint reduces the minter's allowance; the operator can set an absolute quota or increment/decrement it. Regular transfers and burns are not restricted. Restriction code 70 (`CODE_MINTER_ALLOWANCE_EXCEEDED`).

### Changed

- Update contract version in `VersionModule` to `0.4.0`.
- Ownable2Step rule deployments now explicitly advertise ERC-165 `IERC165` (`0x01ffc9a7`), ERC-173 (`0x7f5828d0`), and Ownable2Step (`0x9ab669ef`) interface IDs.
- `RuleMintAllowance` now enforces single-target binding like `RuleConditionalTransferLight`: a second `bindToken` call reverts with `RuleMintAllowance_TokenAlreadyBound` until the current RuleEngine/token is unbound.
- `RuleMintAllowance` no longer advertises the full ERC-3643 `ICompliance` interface through ERC-165 because its mint quota requires spender-aware callbacks.

### Dependencies

- Update RuleEngine to `v3.0.0-rc4`. Role constants were isolated into dedicated storage contracts (`RulesManagementModuleRolesStorage`, `ERC3643ComplianceRolesStorage`); concrete rules that reference `RULES_MANAGEMENT_ROLE` / `COMPLIANCE_MANAGER_ROLE` now inherit the corresponding storage contract.

### Documentation

- Added technical documentation: `doc/technical/RuleConditionalTransferLightMultiToken.md`.
- Updated README operation-rule sections and tables to include `RuleConditionalTransferLightMultiToken`.
- Added technical documentation: `doc/technical/RuleMintAllowance.md`.
- Updated restriction code table, rule index, role summary, and Ownable2Step list in README.
- Documented that `RuleMintAllowance` does not work with pure ERC-3643 3-arg mint callbacks; it requires the spender-aware CMTAT/RuleEngine path.

### Testing

- Added `RuleConditionalTransferLightMultiToken` tests proving approvals are token-scoped and cannot be consumed cross-token.
- Added explicit RuleEngine integration tests for `RuleConditionalTransferLightMultiToken` documenting caller-context behavior in shared RuleEngine topology.
- Added `Ownable2StepERC165Support` test covering all Ownable2Step rule deployments.
- Extended `Ownable2StepERC165Support` with negative assertions to ensure Ownable2Step rule deployments do not advertise unrelated interfaces (`IAccessControl`, `0xdeadbeef`).
- Added unit tests (`test/RuleMintAllowance/RuleMintAllowance.t.sol`, `test/RuleMintAllowance/RuleMintAllowanceOwnable2Step.t.sol`) and CMTAT integration test (`test/RuleMintAllowance/CMTATIntegration.t.sol`) — 54 tests, including batch mint rollback and ERC-165 advertised-interface coverage, >98% line coverage on `RuleMintAllowanceBase`.

## v0.3.0 - 2026-04-16

Commit: `91c21c1191e84ff938892267ec443b0d1bb9efb0`

### Security

Expand Down
Loading
Loading