Skip to content
Merged

Dev #13

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 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
67c9376
docs: add per-rule semantics comparison table
rya-sge Jul 13, 2026
55b350f
docs(RuleConditionalTransferLightMultiToken): document direct-binding…
rya-sge Jul 13, 2026
e79af46
test: add stateful invariant suite for approval conservation and mint…
rya-sge Jul 13, 2026
e893edf
feat(operation rules): add resetApproval and clearMintAllowances for …
rya-sge Jul 13, 2026
83a7d9a
feat(erc165): advertise IAddressList on address-set rules (prerequisi…
rya-sge Jul 13, 2026
0f93969
feat(RuleConditionalTransferLightMultiToken): add caller-explicit de…
rya-sge Jul 13, 2026
eca82be
test: assert tokenId/ITransferContext overload parity across all Rule…
rya-sge Jul 13, 2026
b9549c6
feat(RuleConditionalTransferLight): add bindRuleEngine so transferred…
rya-sge Jul 13, 2026
dc786e6
refactor(access-control): make all _authorize*/_only* hooks internal …
rya-sge Jul 13, 2026
e2271d6
feat(breaking): replace address(0)-membership idiom with explicit all…
rya-sge Jul 13, 2026
0708bbf
feat(breaking): make RuleIdentityRegistry ERC-3643 conformant — veri…
rya-sge Jul 13, 2026
94fc518
docs(security): publish v0.4.0 AI-assisted audit report; sync threat …
rya-sge Jul 14, 2026
19e4728
docs(changelog): consolidate audit remediation into the v0.4.0 relea…
rya-sge Jul 14, 2026
7f017b2
chore(audit): close out the v0.4.0 review — batch adds reject addres…
rya-sge Jul 14, 2026
bd62f94
chore(lint): remove dead RuleTransferValidation import from RuleSpend…
rya-sge Jul 14, 2026
ff041e9
test: close reachable coverage gaps in mint/burn message lookups an…
rya-sge Jul 14, 2026
297cffe
style: replace emoji warning markers with WARNING: in NatSpec; docu…
rya-sge Jul 14, 2026
232a46e
style: fix Solidity style violations across src/ — function order, Na…
rya-sge Jul 14, 2026
6b0c5e2
docs(changelog): add a summary of the main v0.4.0 features at the top…
rya-sge Jul 14, 2026
76c3052
docs(changelog): describe the v0.4.0 review accurately as an internal…
rya-sge Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
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


80 changes: 71 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,62 @@
# 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]`.

Full per-rule semantics (who each rule screens, mint/burn handling, unset-oracle behaviour, stateful?, authoritative view) are tabulated in `doc/technical/RULE_SEMANTICS.md` — consult it before assuming any rule behaves like its siblings.

### Standards conformance (non-negotiable)
Rules that implement a standardized interface must match that standard's semantics, not merely its function signatures. Specs are vendored in `doc/ERCSpecification/` — read them before changing a rule's screening logic.

- **`RuleIdentityRegistry` conforms to ERC-3643 (enforced, I-1).** The spec mandates that **only the receiver** be identity-verified: *"The receiver MUST be whitelisted on the Identity Registry and verified"*; `transferFrom` "works the same way"; `mint` and `forcedTransfer` "only require the receiver"; `burn` "bypasses all checks on eligibility". The sender, the spender and the minter are **not** required to be verified — do not re-add those checks as defaults. Screening the sender **traps de-listed holders** (the spec checks only the receiver precisely so a lapsed investor can still exit their position). Stricter screening is available as an explicit opt-in via the `checkSender` / `checkSpender` flags, both defaulting to `false`.
- **`isVerified(address(0))` must be `false`** — ERC-3643 defines `isVerified` as "is this wallet a valid investor holding the required claims", and `address(0)` is not a wallet. Likewise `RuleERC2980`'s `whitelist(address)` / `frozenlist(address)` are MANDATORY ERC-2980 getters and must not return `true` for `address(0)`. **Enforced (I-12):** mint/burn permission is an explicit `allowMint` / `allowBurn` flag, and the zero address can never enter any list — single adds revert, batch adds skip it. Never re-introduce "whitelist `address(0)` to enable mint/burn".

## 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 +71,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)`. **Direct-binding-only (Topology B)** — approvals are *consumed* under `msg.sender`, so this rule must NOT be added to a RuleEngine; behind an engine it either reverts or loses all per-token isolation. See `RESULT.md` F-4 and `doc/technical/RuleConditionalTransferLightMultiToken.md` |
| `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,27 +98,43 @@ 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, 24 (mint not allowed), 25 (burn not allowed) |
| RuleSanctionsList | 30–32 |
| RuleBlacklist | 36–38 |
| RuleConditionalTransferLight | 46 |
| RuleConditionalTransferLight / …MultiToken | 46 |
| RuleMaxTotalSupply | 50 |
| RuleIdentityRegistry | 55–57 |
| RuleERC2980 | 60–63 |
| RuleERC2980 | 60–63, 64 (mint not allowed), 65 (burn not allowed) |
| 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`).
- **All `_authorize*()` / `_only*()` access-control hooks are `internal view virtual`** — both the abstract declaration and every override. An authorization hook checks and reverts; it must never mutate state, and `view` makes that a compiler-enforced invariant rather than a convention. It is free: these are `internal`, so `view` costs no gas and changes no runtime behaviour. Both OZ check functions (`AccessControl._checkRole`, `Ownable._checkOwner`) are `view`, so every hook can be.
- **One documented exception**: `RuleConditionalTransferLightMultiTokenBase._authorizeComplianceBindingChange` cannot be `view`, because it delegates to `_onlyComplianceManager()`, which `lib/RuleEngine` declares non-`view` (Solidity checks mutability against a virtual's *declared* type, not the installed override). If you hit this constraint elsewhere, document why inline — do not silently drop `view` from a hook.
- 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`.
- Do not create git commits; provide commit messages only when requested.
- Always run full tests (`forge test`) after any code modification, including lint-driven or mechanical refactors, before reporting completion.
- Use `require(condition, CustomError(...))` for custom errors; avoid direct `revert CustomError(...)`.
- **No emoji in code comments or NatSpec.** Use a plain word marker instead: `WARNING:`, `NOTE:`, `IMPORTANT:`. Emoji render inconsistently across editors, terminals, `forge doc` output and diffs; they are not searchable (`grep WARNING` finds the marker, `grep ⚠️` depends on the shell); and they encode as multi-byte sequences that can be silently mangled by tooling. This applies to `src/`, `test/` and `script/`. Markdown documentation may use emoji freely — the restriction is Solidity comments only.
- `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.
Loading
Loading