diff --git a/.gitignore b/.gitignore index 1bb896c..faaf0d1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ docOut/ node_modules/ /history /ERCSpecification +FEEDBACK.md *.dbg.json # Codex @@ -27,3 +28,5 @@ node_modules/ #drawio *.bkp *.dtmp + + diff --git a/AGENTS.md b/AGENTS.md index b151852..bec6fee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 | |---|---| @@ -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/`. @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 68490c9..932d5f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,7 +43,110 @@ 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 + +_Nothing yet._ + +## v0.4.0 - 2026-07-14 + +### Summary + +Two new rule families, two standards-conformance fixes, and hardening from an internal review. + +**New rules** +- **`RuleMintAllowance`** — a per-minter mint quota. The operator sets an absolute quota (or increments/decrements it) and each mint debits the minter's allowance. It is the only rule keyed on the mint `spender`, so it requires the spender-aware CMTAT/RuleEngine callback path. Restriction code `70`. +- **`RuleConditionalTransferLightMultiToken`** — conditional transfers with approvals keyed by `(token, from, to, value)`. **Direct-binding only:** it must not be added to a `RuleEngine`. + +**Breaking: two standards-conformance fixes.** Both change default behaviour and require deployer action — see *Changed* for the migration steps. +- **`RuleIdentityRegistry` now follows ERC-3643 and verifies only the RECEIVER.** It previously screened the sender, the spender and the minter. The sender check was the damaging one: it *trapped de-listed holders*, who could neither receive nor send, when the spec checks only the receiver precisely so a lapsed investor can still exit their position. Stricter screening survives as opt-in `checkSender` / `checkSpender` flags, both defaulting to `false`. +- **Mint/burn permission is now an explicit `allowMint` / `allowBurn` flag**, not membership of `address(0)`. Enabling mint/burn by whitelisting the zero address made mandatory getters lie — `isVerified(address(0))` and `RuleERC2980.whitelist(address(0))` both returned `true` — and made `removeAddress(address(0))` silently halt all issuance. The zero address can no longer enter any list. New codes `24`/`25` and `64`/`65` say "minting is not allowed" instead of the misleading "sender is not whitelisted". + +**Usability and operations** +- `RuleConditionalTransferLight` **works behind a `RuleEngine`**: `bindToken` (the ERC-20 target) and the new `bindRuleEngine` (the authorized caller) are now separate roles, which fixes `approveAndTransferIfAllowed`. +- Caller-explicit pre-flight views (`detectTransferRestrictionForToken` / `canTransferForToken`) so a wallet or explorer gets a truthful answer from the multi-token rule. +- Stale-state cleanup after rebinding: `resetApproval(...)` and `clearMintAllowances(...)`. +- Overflow-safe supply-cap views — they return code `50` instead of reverting with an arithmetic panic. + +**Internal review.** An AI-assisted, test-backed review of `src/` reported 0 Critical/High/Medium, 2 Low and 8 Informational findings; see [`CLAUDE_AUDIT.md`](./doc/security/audits/tools/v0.4.0/claude-audit/CLAUDE_AUDIT.md). This is **not** a formal third-party audit, and the project has not had one. Test suite grew 425 → **511 tests** (97.8% line, 97.3% branch coverage) and now includes a stateful, mutation-verified invariant suite. + +### Added + +- `RuleConditionalTransferLight`: **split the binding into two independent roles**, so the rule works fully behind a `RuleEngine`. New `bindRuleEngine(address)` / `unbindRuleEngine()` (compliance-manager gated, emitting `RuleEngineBound` / `RuleEngineUnbound`) authorise a RuleEngine to call the transfer execution hooks, while `bindToken` continues to designate the **ERC-20 token** the rule acts on. `transferred` now accepts a call from either the bound token or the bound RuleEngine (new `isTransferExecutor(address)` view). This fixes `approveAndTransferIfAllowed`, which was previously unusable behind a RuleEngine: the single binding slot had to be *both* the ERC-20 target and the authorised caller, and behind an engine those are different addresses — binding the engine broke the helper (an engine is not an ERC-20) while binding the token left the engine unauthorised and reverted every transfer and mint. Backward compatible: existing direct-binding deployments are unaffected, and a deployment that binds the engine via `bindToken` keeps working as before (though it should migrate to use both bindings). See finding F-3. +- `RuleConditionalTransferLightMultiToken`: new caller-explicit pre-flight views `detectTransferRestrictionForToken(token, from, to, value)` and `canTransferForToken(token, from, to, value)`. The standardized ERC-1404 / ERC-3643 views (`detectTransferRestriction`, `canTransfer`) derive the token key from `msg.sender`, so an off-chain `eth_call` from a wallet or explorer always read "not approved" even for an approved transfer; the new views take the token explicitly and give every caller the real answer. Additive — the standardized signatures are unchanged, and both paths are backed by the same internal helper so they can never disagree for the bound token. +- ERC-165: `RuleWhitelist`, `RuleBlacklist`, `RuleSpenderWhitelist` (and their `Ownable2Step` variants) now advertise the `IAddressList` interface (`0x5d10e182`). Purely additive — no call is rejected. This is the prerequisite for `RuleWhitelistWrapper` to interface-check its child rules (improvement I-4, finding F-5). Adds `AddressListInterfaceId` (pre-computed constant) and `IAddressListInterfaceIdHelper` (flattened interface used to derive it): `type(IAddressList).interfaceId` cannot be used, because it omits `contains(address)` inherited from `IIdentityRegistryContains`. `RuleWhitelistWrapper` deliberately does **not** advertise it — it exposes no address set of its own, so a wrapper cannot be nested inside another wrapper. +- `RuleConditionalTransferLight` / `RuleConditionalTransferLightMultiToken`: new `resetApproval(...)` operator function that discards **every** outstanding approval for a transfer key in one call (returns the cleared count, emits `TransferApprovalReset`). It deliberately does **not** require a bound token, so it can clean up approvals that survived an `unbindToken` — and, for the multi-token rule, approvals stranded under a key that can never be consumed. +- `RuleMintAllowance`: new `clearMintAllowances(address[] calldata minters)` operator function that zeroes the listed minters' quotas (non-reverting batch), for discarding stale quotas before rebinding. +- `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`). + +### 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 + +- **BREAKING — `RuleIdentityRegistry` is now ERC-3643 conformant: only the RECEIVER is verified.** The rule previously screened the sender, the spender **and** the minter. The specification mandates none of those — *"The receiver MUST be whitelisted on the Identity Registry and verified"*; *"`transferFrom` works the same way"*; *"`mint` and `forcedTransfer` only require the receiver"*; *"The `burn` function bypasses all checks on eligibility"*. + - **The sender check was the most damaging.** ERC-3643 screens only the receiver precisely so that an investor whose identity lapses (expired claim, revoked identity) can still **exit their position** by sending to a verified counterparty. Screening the sender trapped de-listed holders: they could neither receive nor send. + - It also fixes finding **F-1**: an unverified minter can now mint to a verified recipient, as the spec requires (previously reverted with `CODE_ADDRESS_SPENDER_NOT_VERIFIED`). + - Stricter screening remains available as an **explicit opt-in**: new `checkSender` / `checkSpender` flags (constructor parameters, plus `setCheckSender(bool)` / `setCheckSpender(bool)`, emitting `IdentityCheckSenderUpdated` / `IdentityCheckSpenderUpdated`). Both default to `false` — the conformant behaviour. Mint and burn stay exempt from the spender check even when `checkSpender` is on. + - **Constructor:** `RuleIdentityRegistry(admin, identityRegistry, checkSender, checkSpender)` — pass `false, false` for the ERC-3643 default. + - **Deployer migration:** a deployment relying on the old (stricter) behaviour must pass `true, true` to preserve it. No restriction codes changed. +- **BREAKING — `RuleWhitelist`, `RuleWhitelistWrapper`, `RuleERC2980`: mint/burn permission is now an explicit flag, not membership of `address(0)`.** Previously these rules enabled mint/burn by *whitelisting the zero address*, which made the standardized identity getters assert falsehoods: `isVerified(address(0))` returned `true` (ERC-3643 defines `isVerified` as "is this wallet a valid investor holding the required claims" — the zero address is not a wallet), and `RuleERC2980.whitelist(address(0))` returned `true` (a **mandatory** ERC-2980 getter). It also meant `removeAddress(address(0))` silently disabled all minting and burning. + - New state: `allowMint` / `allowBurn`, with `setAllowMint(bool)` / `setAllowBurn(bool)` (admin/owner gated, emitting `AllowMintUpdated` / `AllowBurnUpdated`), so issuance can still be permanently closed at runtime while redemptions stay open. + - New restriction codes: `RuleWhitelist` / `RuleWhitelistWrapper` → `24` (`CODE_MINT_NOT_ALLOWED`) and `25` (`CODE_BURN_NOT_ALLOWED`); `RuleERC2980` → `64` / `65`. A blocked mint now reports "minting is not allowed" instead of the misleading "sender is not whitelisted". + - The zero address can no longer enter any list: both single **and batch** adds revert (`RuleAddressSet_ZeroAddressNotAllowed` / `RuleERC2980_ZeroAddressNotAllowed`). This is the one input a batch does not skip — see the batch note below. + - **Constructors:** `RuleWhitelist(admin, forwarder, checkSpender, allowMintBurn)` keeps its shape — `allowMintBurn` now sets *both* flags instead of whitelisting `address(0)`. `RuleWhitelistWrapper` gains an `allowMintBurn` parameter (it holds no addresses of its own and must decide mint/burn itself). `RuleERC2980`'s third parameter is renamed `allowBurn` → `allowMintBurn` and now governs both operations. + - **Deployer migration:** a deployment that enabled mint/burn by whitelisting `address(0)` must instead pass `allowMintBurn = true` (or call the setters). Permission semantics are otherwise unchanged: a permitted mint still requires a whitelisted **recipient**, and a permitted burn still requires a whitelisted **sender**. + - **Batch adds reject `address(0)` rather than skipping it.** The batch convention still skips *duplicates* (an idempotent no-op the emitted event describes truthfully), but silently dropping the sentinel would make `AddAddresses` / `AddWhitelistAddresses` / `AddFrozenlistAddresses` report a member that is not in the set — re-polluting, off-chain, the very view this change cleans up on-chain. + - **Removed capability (`RuleBlacklist` / `RuleSpenderWhitelist`):** these rules inherit the same address set, so blacklisting `address(0)` — which previously acted as an undocumented "halt all issuance" kill switch, since a mint has `from == address(0)` — is no longer possible. They did **not** receive `allowMint` / `allowBurn` flags: a deny-list has no legitimate reason to list the sentinel, and issuance is better halted at the token (CMTAT pause/deactivate) or with an allow-list rule. If you relied on that idiom, migrate before upgrading. +- Access-control hooks: **all `_authorize*()` / `_only*()` hooks are now `internal view virtual`**, both the abstract declarations and every override. An authorization hook checks and reverts; `view` makes "authorization never mutates state" a compiler-enforced invariant rather than a convention. Compile-time only — `view` on an `internal` function has no gas or runtime impact. Normalised `RuleWhitelistWrapperBase._authorizeCheckSpenderManager` (declaration), the three `RuleWhitelistWrapper` overrides (which disagreed with their own `Ownable2Step` twin), and the six `_onlyComplianceManager` overrides across the operation rules. The convention is now recorded in `CLAUDE.md`/`AGENTS.md`. One documented exception remains: `RuleConditionalTransferLightMultiTokenBase._authorizeComplianceBindingChange` delegates to `_onlyComplianceManager()`, which `lib/RuleEngine` declares non-`view`, and Solidity checks mutability against a virtual's declared type — it can only become `view` once that is changed upstream. +- `RuleSpenderWhitelist` / `RuleSpenderWhitelistOwnable2Step`: remove a dead `RuleTransferValidation` import. The symbol was referenced nowhere in either file (not in the inheritance list, an `override(...)` specifier, or a body). Cosmetic — no bytecode or behavioural change. Surfaced by Aderyn 0.6.5 (`Unused Import`) in the post-remediation re-run. +- `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. +- 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 + +- `RuleConditionalTransferLightMultiToken`: document that the rule is **direct-binding-only** and **must not be added to a `RuleEngine`**. Approvals are recorded under the `token` argument but consumed under `msg.sender`, so behind an engine every wiring either reverts or silently loses per-token isolation. Added a "Deployment topology" section with the exhaustive case analysis to `doc/technical/RuleConditionalTransferLightMultiToken.md`, documented the caller-dependent `detectTransferRestriction`, and propagated the constraint to the README binding-model table, `RULE_SEMANTICS.md` and the project guide. +- `RuleWhitelistWrapper`: document the child-rule scan cost model and publish operator guidance. The wrapper makes one external `STATICCALL` per child (**~8.8k gas each**) and the scan runs during transfer *execution*, so it is paid by the transferring user on every transfer — not only in views. At the default `maxRules = 10` the worst case is ~90k gas per transfer (~121k with `checkSpender`). Two amplifiers are documented: a transfer that will be *rejected* never early-exits and therefore always scans all children, and `checkSpender = true` adds a third target address that must also be resolved. The scan is linear (marginal cost measured flat at ~8.8k gas/child from 25 to 200 children). Guidance: keep the child list at or below the default cap, order children by expected hit rate, and treat raising `maxRules` as a permanent per-transfer cost on every holder (a cap of 100 ⇒ ~884k gas/transfer, measured). This is a cost problem rather than a liveness one — transfers still fit in a block until roughly 3,400 children. The list size remains the **operator's responsibility**; no lower cap is hard-coded. +- Add `doc/technical/INVARIANT_TESTS.md` — documents the stateful invariant suite: handler architecture and ghost variables, each of the four invariants and what it proves, the mutation-testing negative controls, the coverage map against the threat-model invariants, and how to add a new invariant. Linked from a new "Invariant testing" section in the README. +- Add `doc/technical/RULE_SEMANTICS.md` — a per-rule comparison table (who each rule screens for `from` / `to` / spender on `transferFrom` / mint / burn, behaviour when the oracle/registry is unset, stateful?, and which pre-flight view is authoritative), with a highlights summary and link added to the README. +- `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 [`CLAUDE_AUDIT.md`](./doc/security/audits/tools/v0.4.0/claude-audit/CLAUDE_AUDIT.md) — the published AI-assisted security audit report for `v0.4.0` (0 Critical/High/Medium, 2 Low, 8 Info), with invariant verification, access-control verification, the remediation record and the open improvement backlog. Backed by the working deliverables `THREAT_MODEL.md`, `RESULT.md` and `TEST_IMPROVEMENT.md`, plus Slither call-graph / inheritance / function-summary comprehension artifacts. +- 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. +- 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 + +- Add `test/TransferContext/OverloadParity.t.sol` — asserts overload parity across all 7 rules that inherit `RuleNFTAdapter`: every ERC-7943 `tokenId` overload returns exactly what its fungible counterpart returns (`tokenId` is ignored by design), the write overloads accept/reject identically, and the `ITransferContext` struct entrypoints dispatch to the same internal hooks. Closes the per-rule gap for `RuleBlacklist`, `RuleSanctionsList`, `RuleERC2980` and `RuleIdentityRegistry`, and pins threat `AC-5` (the `ctx` entrypoints are unguarded but view-only on validation rules, so they mutate nothing). +- Add `test/invariant/` — the project's first stateful invariant suite (`StdInvariant`, `fail_on_revert = true`). Handler-driven fuzzing over the two stateful rules: `RuleConditionalTransferLight` (approval conservation, `INV-5`) and `RuleMintAllowance` (exact quota accounting via a ghost mirror, `INV-7`). 4 invariants × 8 192 calls each, 0 reverts. The handlers also prove mint/burn never consume an approval and that non-mint transfers never touch a quota. Both invariants were mutation-verified (injecting an approval double-spend and an off-by-one quota deduction makes them fail). Adds an `[invariant]` section to `foundry.toml`. +- 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. Tests suffixed `_CurrentBehaviour` assert behaviour the audit considers wrong: fixing the underlying issue must make them fail. +- Add `test/MintBurnFlags/MintBurnFlags.t.sol` and `test/RuleConditionalTransferLightMultiToken/MultiTokenSurface.t.sol` — cover the new `allowMint` / `allowBurn` setters, their access control and events, the dedicated mint/burn restriction codes, and the multi-token pre-flight surface. Also `test/InterfaceId/AddressListInterfaceId.t.sol`, `test/StaleState/StaleStateHygiene.t.sol`, `test/RuleConditionalTransferLight/RuleConditionalTransferLightBindRuleEngine.t.sol`. +- Close the last reachable coverage gaps introduced by this release: the `messageForTransferRestriction` lookups for the new mint/burn codes (`24`/`25` on the whitelist family, `64`/`65` on ERC-2980) and the wrapper's degenerate `from == 0 && to == 0` branch — the anti-drift guard that keeps `RuleWhitelistWrapper` and `RuleWhitelistBase` agreeing — were all reachable but unexecuted. 5 tests added; **every uncovered line in `src/` is now an abstract declaration with no body.** +- Regenerate the coverage report in `doc/coverage` (`forge coverage --report lcov` + `genhtml`). +- **Full suite: 511 tests across 78 suites — 97.8% line coverage (1 082/1 106), 97.3% branch coverage (220/226)** (up from 425 tests / 94.91% lines). +- 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 diff --git a/CLAUDE.md b/CLAUDE.md index b151852..bec6fee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 | |---|---| @@ -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/`. @@ -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. diff --git a/README.md b/README.md index 097c66c..dedd943 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,33 @@ Each rule can be used **standalone**, directly plugged into a CMTAT token, **or** managed collectively via a RuleEngine. +The **RuleEngine** is an external smart contract that applies transfer restrictions to security tokens such as **CMTAT** or [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643)-compatible tokens through a RuleEngine. +Rules are modular validator contracts that the `RuleEngine` or `CMTAT` compatible token can call on every transfer to ensure regulatory and business-logic compliance. + +**Current package version:** `v0.4.0` (contracts report `version()` → `"0.4.0"`). Built against CMTAT `v3.3.0-rc1` and RuleEngine `v3.0.0-rc4`; see [Compatibility](#compatibility) for the supported range. + > This project has not undergone an audit and is provided as-is without any warranties. +## Table of Contents + +- [Schema](#schema) +- [Overview](#overview) +- [Compatibility](#compatibility) +- [Specifications](#specifications) +- [Architecture](#architecture) +- [Types of Rules](#types-of-rules) +- [Quick Start](#quick-start) +- [Deployment Guide](#deployment-guide) +- [Rules details](#rules-details) +- [Access Control](#access-control) +- [Toolchains and Usage](#toolchains-and-usage) +- [API](#api) +- [Security](#security) +- [Intellectual property](#intellectual-property) + ## Schema -- Using rules with CMTAT and ERC-3643 tokens through a [RuleEngine](ttps://github.com/CMTA/RuleEngine) +- Using rules with CMTAT and ERC-3643 tokens through a [RuleEngine](https://github.com/CMTA/RuleEngine) ![Rule-RuleEngine.drawio](./doc/schema/Rule-RuleEngine.drawio.png) @@ -16,15 +38,8 @@ Each rule can be used **standalone**, directly plugged into a CMTAT token, **or* ![Rule-Rule.drawio](./doc/schema/Rule-Rule.drawio.png) -## Table of Contents - -[TOC] - ## Overview -The **RuleEngine** is an external smart contract that applies transfer restrictions to security tokens such as **CMTAT** or [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643)-compatible tokens through a RuleEngine. -Rules are modular validator contracts that the `RuleEngine` or `CMTAT` compatible token can call on every transfer to ensure regulatory and business-logic compliance. - ### Key Concepts - **Rules are controllers** that validate or modify token transfers. @@ -38,51 +53,51 @@ Rules are modular validator contracts that the `RuleEngine` or `CMTAT` compatibl - Conditional approvals - Arbitrary compliance logic -## Quick Start - -```bash -# 1. Clone the repository -git clone -cd Rules - -# 2. Install Foundry (if not already installed) -# https://book.getfoundry.sh/getting-started/installation +### Integration modes -# 3. Install submodule dependencies -forge install +A rule can be consumed in three ways. All three call the same rule contract; they differ only in who calls it and how much of the compliance interface is required. -# 4. Compile -forge build +| Mode | Caller | What the rule must implement | When to use | +| --- | --- | --- | --- | +| **Direct CMTAT rule** | A CMTAT token calls the rule directly (no RuleEngine) | `IRuleEngine` (`canTransfer` + `transferred`, including the spender-aware overload) | A single rule is enough; no multi-rule orchestration needed | +| **RuleEngine-managed rule** | A `RuleEngine` aggregates one or more rules and calls each on every transfer | `IRule` (`IRuleEngine` + `canReturnTransferRestrictionCode`) | Several rules must be combined, ordered, or share restriction codes | +| **ERC-3643 through RuleEngine** | An ERC-3643 token drives `created` / `destroyed` / transfer hooks on a RuleEngine, which forwards them to the rules | Rules as above; the **RuleEngine** implements the full ERC-3643 `ICompliance` | The token is ERC-3643 and needs full `ICompliance` — a standalone rule cannot back an ERC-3643 token directly | -# 5. Run tests -forge test -``` +Interface details for each mode are documented under [Architecture](#architecture); full signatures live in the [API](#api) reference. ## Compatibility -| Component | Compatible Versions | -| ---------------- | ----------------------------------------- | -| **Rules v0.1.0** | CMTAT ≥ v3.0.0
RuleEngine v3.0.0-rc2 | +| Component | Compatible Versions | +| ---------------- | ---------------------------------------------------------- | +| **Rules v0.4.0** | CMTAT ≥ v3.0.0 (tested against v3.3.0-rc1)
RuleEngine v3.0.0-rc4 | + +Spender-aware paths (e.g. `RuleMintAllowance`) rely on the 4-argument `canTransferFrom` / `transferred(spender, from, to, value)` callbacks, which require a CMTAT / RuleEngine that forwards the spender to the rule; this repository is validated against CMTAT `v3.3.0-rc1`. The other rules only use the 3-argument path and work across the full CMTAT ≥ v3.0.0 range. Each Rule implements the interface `IRuleEngine` defined in CMTAT. -This interface declares the ERC-3643 functions `transferred`(read-write) and `canTransfer`(read-only) with several other functions related to [ERC-1404](https://github.com/ethereum/eips/issues/1404), [ERC-7551](https://ethereum-magicians.org/t/erc-7551-crypto-security-token-smart-contract-interface-ewpg-reworked/25477) and [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643). +This interface declares the ERC-3643 functions `transferred` (read-write) and `canTransfer` (read-only) with several other functions related to [ERC-1404](https://github.com/ethereum/eips/issues/1404), [ERC-7551](https://ethereum-magicians.org/t/erc-7551-crypto-security-token-smart-contract-interface-ewpg-reworked/25477) and [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643). ## Specifications ### ERC-3643 -Each rule implements the following functions from the ERC-3643 `ICompliance`interface +Each rule implements the following functions from the ERC-3643 `ICompliance` interface ```solidity function canTransfer(address _from, address _to, uint256 _amount) external view returns (bool); function transferred(address _from, address _to, uint256 _amount) external; ``` -However, contrary to the RuleEngine, the whole interface is currently not implemented (e.g. `created`and `destroyed`) and as a result, the rule can not directly support ERC-3643 token. +However, contrary to the RuleEngine, the whole interface is currently not implemented (e.g. `created` and `destroyed`) and as a result, the rule cannot directly support ERC-3643 token. The alternative to use a Rule with an ERC-3643 token is through the RuleEngine, which implements the whole `ICompliance` interface. +The diagram below shows the recommended integration: the ERC-3643 token drives transfer, mint (`created`) and burn (`destroyed`) compliance hooks on the RuleEngine, which forwards them to the rules. A rule used on its own only implements `canTransfer` + `transferred`, so it cannot back an ERC-3643 token directly. + +![Using a rule with an ERC-3643 token through a RuleEngine](./doc/img/readme-erc3643-integration.png) + +_Diagram source: doc/img/readme-erc3643-integration.puml._ + ### ERC-721/ERC-1155 To improve compatibility with [ERC-721](https://eips.ethereum.org/EIPS/eip-721) and [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155), most validation rules implement the interface `IERC7943NonFungibleComplianceExtend` which includes compliance functions with the `tokenId` argument. Operation rules (such as `RuleConditionalTransferLight`) are ERC-20 only and do not expose the ERC-721/1155 interfaces. `RuleMaxTotalSupply` is ERC-20 only as well and does not expose ERC-721/1155 interfaces. @@ -92,19 +107,25 @@ While no rules currently apply restriction on the token id, the validation inter ```solidity // IERC7943NonFungibleCompliance interface // Read-only functions -function canTransfer(address from, address to, uint256 tokenId, uint256 amount)external view returns (bool allowed) +function canTransfer(address from, address to, uint256 tokenId, uint256 amount) external view returns (bool allowed) // IERC7943NonFungibleComplianceExtend interface // Read-only functions -function detectTransferRestriction(address from, address to, uint256 tokenId, uint256 amount)external view returns (uint8 code); -function detectTransferRestrictionFrom(address spender, address from, address to, uint256 tokenId, uint256 value)external view returns (uint8 code); -function canTransferFrom(address spender, address from, address to, uint256 tokenId, uint256 value)external returns (bool allowed); +function detectTransferRestriction(address from, address to, uint256 tokenId, uint256 amount) external view returns (uint8 code); +function detectTransferRestrictionFrom(address spender, address from, address to, uint256 tokenId, uint256 value) external view returns (uint8 code); +function canTransferFrom(address spender, address from, address to, uint256 tokenId, uint256 value) external returns (bool allowed); // State modifying functions (write) function transferred(address from, address to, uint256 tokenId, uint256 value) external; function transferred(address spender, address from, address to, uint256 tokenId, uint256 value) external; ``` +The diagram below shows a non-fungible transfer flowing through the `tokenId`-aware compliance signatures. For validation rules a single `transferred(...)` call both validates and reverts — it internally runs `detectTransferRestrictionFrom` and requires `TRANSFER_OK` — so no separate pre-check is required in the transfer path; the read-only `detectTransferRestriction*` / `canTransfer*` overloads remain available for off-chain queries. The `RuleNFTAdapter` carries the `tokenId` argument but currently delegates to the address-based checks (`from` / `to` / `spender`), so no rule restricts on the token id yet. + +![ERC-721 / ERC-1155 compliance interface flow](./doc/img/readme-erc721-erc1155-compliance.png) + +_Diagram source: doc/img/readme-erc721-erc1155-compliance.puml._ + ## Architecture @@ -134,36 +155,41 @@ function transferred(address spender, address from, address to, uint256 tokenId, Here is the list of codes used by the different rules -| Contract | Constant name | Value | -| ----------------------- | ------------------------------------ | ----- | -| All | TRANSFER_OK (from CMTAT) | 0 | -| RuleWhitelist | CODE_ADDRESS_FROM_NOT_WHITELISTED | 21 | -| | CODE_ADDRESS_TO_NOT_WHITELISTED | 22 | -| | CODE_ADDRESS_SPENDER_NOT_WHITELISTED | 23 | -| | Reserved slot | 24-29 | -| RuleSanctionList | CODE_ADDRESS_FROM_IS_SANCTIONED | 30 | -| | CODE_ADDRESS_TO_IS_SANCTIONED | 31 | -| | CODE_ADDRESS_SPENDER_IS_SANCTIONED | 32 | -| | Reserved slot | 33-35 | -| RuleBlacklist | CODE_ADDRESS_FROM_IS_BLACKLISTED | 36 | -| | CODE_ADDRESS_TO_IS_BLACKLISTED | 37 | -| | CODE_ADDRESS_SPENDER_IS_BLACKLISTED | 38 | -| | Reserved slot | 39-45 | -| RuleConditionalTransferLight | CODE_TRANSFER_REQUEST_NOT_APPROVED | 46 | -| | Reserved slot | 47-49 | -| RuleMaxTotalSupply | CODE_MAX_TOTAL_SUPPLY_EXCEEDED | 50 | -| | Reserved slot | 51-54 | -| RuleIdentityRegistry | CODE_ADDRESS_FROM_NOT_VERIFIED | 55 | -| | CODE_ADDRESS_TO_NOT_VERIFIED | 56 | -| | CODE_ADDRESS_SPENDER_NOT_VERIFIED | 57 | -| | Reserved slot | 58-59 | -| RuleERC2980 | CODE_ADDRESS_FROM_IS_FROZEN | 60 | -| | CODE_ADDRESS_TO_IS_FROZEN | 61 | -| | CODE_ADDRESS_SPENDER_IS_FROZEN | 62 | -| | CODE_ADDRESS_TO_NOT_WHITELISTED | 63 | -| | Reserved slot | 64-65 | -| RuleSpenderWhitelist | CODE_ADDRESS_SPENDER_NOT_WHITELISTED | 66 | -| | Reserved slot | 67-70 | +| Contract | Constant name | Value | +| ---------------------------- | ------------------------------------ | ----- | +| All | TRANSFER_OK (from CMTAT) | 0 | +| RuleWhitelist | CODE_ADDRESS_FROM_NOT_WHITELISTED | 21 | +| | CODE_ADDRESS_TO_NOT_WHITELISTED | 22 | +| | CODE_ADDRESS_SPENDER_NOT_WHITELISTED | 23 | +| | CODE_MINT_NOT_ALLOWED | 24 | +| | CODE_BURN_NOT_ALLOWED | 25 | +| | Reserved slot | 26-29 | +| RuleSanctionList | CODE_ADDRESS_FROM_IS_SANCTIONED | 30 | +| | CODE_ADDRESS_TO_IS_SANCTIONED | 31 | +| | CODE_ADDRESS_SPENDER_IS_SANCTIONED | 32 | +| | Reserved slot | 33-35 | +| RuleBlacklist | CODE_ADDRESS_FROM_IS_BLACKLISTED | 36 | +| | CODE_ADDRESS_TO_IS_BLACKLISTED | 37 | +| | CODE_ADDRESS_SPENDER_IS_BLACKLISTED | 38 | +| | Reserved slot | 39-45 | +| RuleConditionalTransferLight | CODE_TRANSFER_REQUEST_NOT_APPROVED | 46 | +| | Reserved slot | 47-49 | +| RuleMaxTotalSupply | CODE_MAX_TOTAL_SUPPLY_EXCEEDED | 50 | +| | Reserved slot | 51-54 | +| RuleIdentityRegistry | CODE_ADDRESS_FROM_NOT_VERIFIED | 55 | +| | CODE_ADDRESS_TO_NOT_VERIFIED | 56 | +| | CODE_ADDRESS_SPENDER_NOT_VERIFIED | 57 | +| | Reserved slot | 58-59 | +| RuleERC2980 | CODE_ADDRESS_FROM_IS_FROZEN | 60 | +| | CODE_ADDRESS_TO_IS_FROZEN | 61 | +| | CODE_ADDRESS_SPENDER_IS_FROZEN | 62 | +| | CODE_ADDRESS_TO_NOT_WHITELISTED | 63 | +| | CODE_MINT_NOT_ALLOWED | 64 | +| | CODE_BURN_NOT_ALLOWED | 65 | +| RuleSpenderWhitelist | CODE_ADDRESS_SPENDER_NOT_WHITELISTED | 66 | +| | Reserved slot | 67-69 | +| RuleMintAllowance | CODE_MINTER_ALLOWANCE_EXCEEDED | 70 | +| | Reserved slot | 71-74 | Note: @@ -235,62 +261,37 @@ The RuleEngine can then: - Return restriction codes - Mutate rule state (operation rules) -#### CMTAT - -Each rule can be directly plugged to a CMTAT token similar to a RuleEngine. - -Indeed, each rule implements the required interface (`IRuleEngine`) with notably the following function as entrypoint. - -```solidity -function transferred(address from,address to,uint256 value) -function transferred(address spender,address from,address to,uint256 value) -``` - -```solidity -/* -* @title Minimum interface to define a RuleEngine -*/ -interface IRuleEngine is IERC1404Extend, IERC7551Compliance, IERC3643IComplianceContract { - /** - * @notice - * Function called whenever tokens are transferred from one wallet to another - * @dev - * Must revert if the transfer is invalid - * Same name as ERC-3643 but with one supplementary argument `spender` - * This function can be used to update state variables of the RuleEngine contract - * This function can be called ONLY by the token contract bound to the RuleEngine - * @param spender spender address (sender) - * @param from token holder address - * @param to receiver address - * @param value value of tokens involved in the transfer - */ - function transferred(address spender, address from, address to, uint256 value) external; -} -``` - +The same rule can also be plugged **directly** into a CMTAT token (see [Rules as Standalone Compliance Contracts](#rules-as-standalone-compliance-contracts) above): the direct-CMTAT path only requires `IRuleEngine`, while the RuleEngine-managed path additionally requires `IRule`. Full signatures for both interfaces are documented in the [API](#api) reference (`IRuleEngine`, `IERC1404Extend`, `IERC7551Compliance`, `IERC3643IComplianceContract`). +## Types of Rules -#### RuleEngine - -For a RuleEngine, each rule implements also the required entry point similar to CMTAT, and as well some specific interface for the RuleEngine through the implementation of `IRule` interface defined in the RuleEngine repository. +There are two categories of rules: validation rules (read-only) and operation rules (read-write). -```solidity -interface IRule is IRuleEngine { - /** - * @dev Returns true if the restriction code exists, and false otherwise. - */ - function canReturnTransferRestrictionCode( - uint8 restrictionCode - ) external view returns (bool); -} +### Which rule should I use? -``` +| Need | Rule | +| --- | --- | +| Only approved holders can send/receive | `RuleWhitelist` | +| Combine several whitelists (OR logic) | `RuleWhitelistWrapper` | +| Restrict `transferFrom` operators (spenders) | `RuleSpenderWhitelist` | +| Block known bad addresses | `RuleBlacklist` | +| Block sanctioned addresses (Chainalysis oracle) | `RuleSanctionsList` | +| Cap total token supply | `RuleMaxTotalSupply` | +| Require identity-registry verification (ERC-3643) | `RuleIdentityRegistry` | +| ERC-2980 Swiss compliance (whitelist + frozenlist) | `RuleERC2980` | +| Require operator approval per transfer | `RuleConditionalTransferLight` | +| Per-transfer approval across several **directly-bound** tokens (not behind a RuleEngine) | `RuleConditionalTransferLightMultiToken` | +| Limit mint quota per minter | `RuleMintAllowance` | +Each rule is also available in `Ownable2Step` and `AccessControl` variants; see [Choosing a Rule Variant](#choosing-a-rule-variant). Stateful rules have binding constraints — see the [Binding model](#binding-model) table. +### How rules differ (semantics comparison) -## Types of Rules +Rules do **not** all treat the spender, mint/burn, or an unset oracle the same way. The full side-by-side table — who each rule screens (`from` / `to` / spender on `transferFrom` / mint / burn), how it behaves when its oracle/registry is unset, whether it is stateful, and which pre-flight view is authoritative — is in **[RULE_SEMANTICS.md](./doc/technical/RULE_SEMANTICS.md)**. The differences most likely to surprise an integrator: -There are two categories of rules: validation rules (read-only) and operation rules (read-write). +- **Spender on mint.** `RuleWhitelist` / `RuleWhitelistWrapper` / `RuleSpenderWhitelist` **exempt** the minter; `RuleBlacklist` / `RuleSanctionsList` **screen** it (deny-list, by design); `RuleIdentityRegistry` also screens it, so the minter must itself be identity-verified; `RuleMintAllowance` **debits the minter's quota**. +- **Unset oracle/registry.** `RuleSanctionsList` (oracle unset) and `RuleIdentityRegistry` (registry unset) **fail open** — all transfers pass. An empty `RuleWhitelistWrapper` **fails closed**. +- **Authoritative pre-flight view.** For `RuleMintAllowance`, `canTransfer` is not authoritative — use `canTransferFrom`. For `RuleConditionalTransferLightMultiToken`, `detectTransferRestriction` is `msg.sender`-dependent. ### Validation Rules (Read-Only) @@ -306,12 +307,34 @@ Available validation rules: `RuleWhitelist`, `RuleWhitelistWrapper`, `RuleSpende Operation rules modify blockchain state during transfer execution. Their `transferred()` function is state-mutating: it consumes or updates stored data as part of the transfer flow. -Available operation rules: `RuleConditionalTransferLight`. +Available operation rules: `RuleConditionalTransferLight`, `RuleConditionalTransferLightMultiToken`, `RuleMintAllowance`. A full-featured variant, `RuleConditionalTransfer`, is maintained as a separate experimental repository at [CMTA/RuleConditionalTransfer](https://github.com/CMTA/RuleConditionalTransfer). +## Quick Start + +```bash +# 1. Clone the repository +git clone +cd Rules + +# 2. Install Foundry (if not already installed) +# https://book.getfoundry.sh/getting-started/installation + +# 3. Install submodule dependencies +forge install + +# 4. Compile +forge build + +# 5. Run tests +forge test +``` + ## Deployment Guide +> ⚠️ **Before production deployment:** this project has [not undergone an audit](#ruleengine---rules). Review the unaudited status, configure roles with least privilege (grant only the roles each operator needs, and prefer the `Ownable2Step` variants for single-owner setups), and run an end-to-end transfer test on the target token setup. + 1. Deploy the rule contract(s) with the desired admin and optional module addresses. 2. Configure the rule state and roles, including whitelist/blacklist entries and oracle or registry addresses. 3. Add rules to the RuleEngine, or set the rule directly on the CMTAT token. @@ -351,7 +374,7 @@ Several rules are available in multiple access-control variants. Use the simples ### Summary tab -| Rule | Type
[read-only / read-write] | ERC-721 / ERC-1155 | ERC-3643 | Security Audit planned in the roadmap | Description | +| Rule | Type
[read-only / read-write] | ERC-721 / ERC-1155 | ERC-3643 via RuleEngine / CMTAT path * | Security Audit planned in the roadmap | Description | | ------------------------------------------------------------ | ------------------------------------ | ------------------ | -------- | ------------------------------------- | ------------------------------------------------------------ | | RuleWhitelist | Read-only | | | | This rule can be used to restrict transfers from/to only addresses inside a whitelist. | | RuleWhitelistWrapper | Read-Only | | | | This rule can be used to restrict transfers from/to only addresses inside a group of whitelist rules managed by different operators. | @@ -362,10 +385,16 @@ Several rules are available in multiple access-control variants. Use the simples | RuleSpenderWhitelist | Read-Only | | | | This rule blocks `transferFrom` when the spender is not in the whitelist. Direct transfers are always allowed. | | RuleERC2980 | Read-Only | | | | ERC-2980 Swiss Compliant rule combining a whitelist (recipient-only) and a frozenlist (blocks sender, recipient, and spender for `transferFrom`). Frozenlist takes priority over whitelist. | | RuleConditionalTransferLight | Read-Write | | | | This rule requires that transfers have to be approved by an operator before being executed. Each approval is consumed once and the same transfer can be approved multiple times. | +| RuleConditionalTransferLightMultiToken | Read-Write | | | | Multi-token variant of ConditionalTransferLight. Approvals are token-scoped with key `(token, from, to, value)` so one token cannot consume another token's approvals. | +| RuleMintAllowance | Read-Write | | Partial | | Enforces a per-minter mint quota managed by an operator; each mint reduces the minter's allowance. Regular transfers and burns are not restricted. | | [RuleConditionalTransfer](https://github.com/CMTA/RuleConditionalTransfer) (external) | Read-Write | | |
(experimental rule) | Full-featured approval-based transfer rule implementing Swiss law *Vinkulierung*. Supports automatic approval after three months, automatic transfer execution, and a conditional whitelist for address pairs that bypass approval. Maintained in a separate repository. | | [RuleSelf](https://github.com/rya-sge/ruleself) (community) | — | | — |
(community project) | Use [Self](https://self.xyz), a zero-knowledge identity solution to determine which is allowed to interact with the token.
Community-maintained rule project. Not developed or maintained by CMTA. | -All rules are compatible with CMTAT, as noted earlier in this README. +All rules implement the CMTAT rule interfaces needed by their supported transfer paths. Some operation rules require the spender-aware callback, as documented in their rule-specific notes. + +* A checkmark in this column means the rule enforces compliance for ERC-3643 tokens **through a RuleEngine or the CMTAT transfer path** — it does **not** mean the rule is itself a full ERC-3643 `ICompliance` contract. A standalone rule implements only `canTransfer` + `transferred`, so it cannot back an ERC-3643 token directly; use it through a RuleEngine, which implements the full `ICompliance` interface (see [Integration modes](#integration-modes)). + + `RuleMintAllowance` is **Partial**: it does not advertise the full ERC-3643 `ICompliance` interface via ERC-165 because its per-minter mint quota requires the spender-aware mint callback to identify the minter, which the 3-argument ERC-3643 mint callback cannot provide. ### Technical documentation @@ -382,9 +411,23 @@ Detailed technical documentation for each rule is available in [`doc/technical/` | RuleSpenderWhitelist | [RuleSpenderWhitelist.md](./doc/technical/RuleSpenderWhitelist.md) | | RuleERC2980 | [RuleERC2980.md](./doc/technical/RuleERC2980.md) | | RuleConditionalTransferLight | [RuleConditionalTransferLight.md](./doc/technical/RuleConditionalTransferLight.md) | +| RuleConditionalTransferLightMultiToken | [RuleConditionalTransferLightMultiToken.md](./doc/technical/RuleConditionalTransferLightMultiToken.md) | +| RuleMintAllowance | [RuleMintAllowance.md](./doc/technical/RuleMintAllowance.md) | ### Operational Notes +#### Binding model + +Stateful (operation) rules restrict which caller may consume their state via `transferred()`, so the target must be explicitly bound with `bindToken`. The binding model differs per rule: + +| Rule | Binding model | Notes | +| --- | --- | --- | +| `RuleConditionalTransferLight` | Single token **+ optional RuleEngine** | Two independent bindings: `bindToken(token)` sets the ERC-20 this rule acts on, `bindRuleEngine(engine)` authorises the engine to call `transferred`. `transferred` accepts either. Behind a RuleEngine, bind **both** — then `approveAndTransferIfAllowed` works too. Rebind only after `unbindToken` / `unbindRuleEngine`. See [Binding: token vs RuleEngine](./doc/technical/RuleConditionalTransferLight.md#binding-token-vs-ruleengine) | +| `RuleConditionalTransferLightMultiToken` | **Multiple direct tokens only** | Approvals keyed by `(token, from, to, value)` but *consumed* under `msg.sender`. ⚠️ **Do not add this rule to a `RuleEngine`** — bind each token directly (`CMTAT.setRuleEngine(rule)`). Behind an engine the rule either reverts or silently loses all per-token isolation; see [Deployment topology](./doc/technical/RuleConditionalTransferLightMultiToken.md#deployment-topology--why-a-ruleengine-does-not-work) | +| `RuleMintAllowance` | Single RuleEngine/token | Bind the RuleEngine address in a CMTAT + RuleEngine setup; rebind only after `unbindToken`. Requires the spender-aware mint callback | + +Validation (read-only) rules have no binding requirement: they hold no per-transfer state and can be shared across tokens and RuleEngines freely. + #### RuleIdentityRegistry - `RuleIdentityRegistry`: allows burns (`to == address(0)`) even if the sender is not verified. This matters only if the token allows self-burn. @@ -403,7 +446,10 @@ Detailed technical documentation for each rule is available in [`doc/technical/` #### RuleWhitelistWrapper -- `RuleWhitelistWrapper`: requires child rules that implement `IAddressList`. Gas cost grows with the number of rules, and a wrapper with zero rules rejects all transfers. +- `RuleWhitelistWrapper`: requires child rules that implement `IAddressList`. A wrapper with zero rules rejects all transfers (fail-closed). +- **Scan cost is paid on every transfer, by the transferring user.** The wrapper makes one external `STATICCALL` per child rule — **~8.8k gas each** — and the scan runs during transfer *execution*, not only in views. At the default cap of 10 children the worst case is ~90k gas per transfer (~121k with `checkSpender = true`). +- **Two amplifiers:** a transfer that is going to be *rejected* never resolves its target addresses, so it never early-exits and always scans **all** children — the failing path is the most expensive one. And `checkSpender = true` adds a third address that must also be found, lowering the early-exit rate. +- **Operator responsibility:** keep the child list at or below the default `maxRules = 10`, and order children by expected hit rate so the early exit fires sooner. The scan is linear (~8.8k gas/child, measured flat up to 200 children), so `setMaxRules` accepts any non-zero value and raising the cap to 100 makes every transfer cost ~884k gas. That is a permanent tax on holders rather than a broken token — transfers still fit in a block until ~3,400 children — but it cannot be undone for transfers already paid. Full cost model and guidance: [RuleWhitelistWrapper.md](./doc/technical/RuleWhitelistWrapper.md#gas-cost-of-the-child-rule-scan). #### RuleSpenderWhitelist @@ -422,17 +468,26 @@ Detailed technical documentation for each rule is available in [`doc/technical/` - `RuleConditionalTransferLight`: `transferred()` is restricted to the single token bound via `bindToken`; second bind reverts with `RuleConditionalTransferLight_TokenAlreadyBound` until `unbindToken`. - `RuleConditionalTransferLight`: mints (`from == address(0)`) and burns (`to == address(0)`) are exempt from approval checks; `created` and `destroyed` delegate to `_transferred`. +#### RuleConditionalTransferLightMultiToken + +- `RuleConditionalTransferLightMultiToken`: approvals are keyed by `(token, from, to, value)` and are not nonce-based. +- `RuleConditionalTransferLightMultiToken`: operator functions are token-scoped (`approveTransfer(token, ...)`, `cancelTransferApproval(token, ...)`, `approvedCount(token, ...)`, `approveAndTransferIfAllowed(token, ...)`). +- `RuleConditionalTransferLightMultiToken`: execution is restricted to bound tokens; only the calling bound token can consume approvals for its own key space. +- `RuleConditionalTransferLightMultiToken`: mints (`from == address(0)`) and burns (`to == address(0)`) are exempt from approval checks; `created` and `destroyed` delegate to `_transferred`. +- `RuleConditionalTransferLightMultiToken`: with a shared `RuleEngine`, the caller seen by the rule is the engine address (not the underlying token). In that topology, token-scoped approvals are not visible unless approvals are keyed to the engine address, which is not per-token scoping. +- **Warning**: `RuleConditionalTransferLightMultiToken` supports several tokens when integrated directly with each token contract. It must not be used for per-token approval isolation through a shared `RuleEngine`. + #### General notes - All validation rules: read-only rules still implement `transferred()` for ERC-3643 and RuleEngine compatibility, but do not change state. - All AccessControl variants: use `onlyRole(ROLE)` in `_authorize*()` and mark internal helpers `virtual`. - All AccessControl variants: use `AccessControlEnumerable`, so role members can be enumerated with `getRoleMember` / `getRoleMemberCount`; default admin is treated as having all roles via `hasRole`, but may not appear in role member lists unless explicitly granted. - All meta-tx-enabled rules: `forwarderIrrevocable` is accepted as-is (including `address(0)`) and is not validated against ERC-165 because some forwarders do not implement it. -- All rules: implement `IERC3643Version` via `VersionModule` and expose `version()` returning `"0.3.0"`. +- All rules: implement `IERC3643Version` via `VersionModule` and expose `version()` returning `"0.4.0"`. ### Read-only (validation) rule -Currently, there are eight validation rules: whitelist, whitelistWrapper, spender whitelist, blacklist, sanctionlist, max total supply, identity registry, and ERC-2980. +Currently, there are eight validation rules: whitelist, whitelist wrapper, spender whitelist, blacklist, sanctions list, max total supply, identity registry, and ERC-2980. #### Whitelist @@ -443,8 +498,10 @@ Only whitelisted addresses may hold or receive tokens. - `to` is not whitelisted The rule is read-only: it only checks stored state. -- Constructor parameter `allowMintBurn` can pre-list `address(0)` for mint/burn flows. -- `allowMintBurn = false` keeps legacy behavior (operator adds `address(0)` manually if needed). +- Constructor parameter `allowMintBurn` sets **both** `allowMint` and `allowBurn` — the common case. Use `setAllowMint(bool)` / `setAllowBurn(bool)` afterwards for independent control (e.g. permanently close issuance while keeping redemptions open). +- Mint/burn permission is an **explicit flag**, never list membership of `address(0)`. The zero address can never enter the list (`addAddress(address(0))` reverts), so `isVerified(address(0))` / `contains(address(0))` stay `false`, as ERC-3643 requires. +- The flag gates the **operation only**: a permitted mint still requires a whitelisted *recipient*; a permitted burn still requires a whitelisted *sender*. +- Blocked mint/burn return dedicated codes `24` / `25` (not the misleading "sender not whitelisted"). **Example** @@ -513,12 +570,13 @@ Implements the [ERC-2980](https://eips.ethereum.org/EIPS/eip-2980) Swiss Complia - **Whitelist**: only whitelisted addresses may *receive* tokens. Senders do not need to be whitelisted and may freely transfer tokens they already hold. - **Frozenlist**: frozen addresses are completely blocked — they can neither send nor receive tokens. Additionally, a frozen address acting as a `transferFrom` spender will have the transfer rejected (code 62), even if `from` and `to` are not frozen. - **Priority**: frozenlist is checked first. If `from`, `to`, or `spender` is frozen, the transfer is rejected regardless of whitelist membership. -- **Burn/redemption handling**: burns (`to == address(0)`) follow the same recipient whitelist check. Constructor parameter `allowBurn` controls whether `address(0)` is whitelisted at deployment. - - `allowBurn = false` (default-safe): burns are blocked with code 63. - - `allowBurn = true`: burns are allowed because `address(0)` is whitelisted. +- **Mint/burn handling**: governed by the explicit `allowMint` / `allowBurn` flags, never by whitelisting `address(0)`. The zero address can never enter either list, so the **mandatory ERC-2980 getters** `whitelist(address(0))` / `frozenlist(address(0))` always return `false`. + - `allowMintBurn = false` (default-safe): mint is refused with code **64**, burn with code **65**. + - `allowMintBurn = true`: both permitted. A permitted mint still requires the recipient to be whitelisted and not frozen; a permitted burn still requires the sender not to be frozen. + - Independently settable afterwards via `setAllowMint(bool)` / `setAllowBurn(bool)`. - Constructors: - - `RuleERC2980(address admin, address forwarderIrrevocable, bool allowBurn)` - - `RuleERC2980Ownable2Step(address owner, address forwarderIrrevocable, bool allowBurn)` + - `RuleERC2980(address admin, address forwarderIrrevocable, bool allowMintBurn)` + - `RuleERC2980Ownable2Step(address owner, address forwarderIrrevocable, bool allowMintBurn)` ![surya_inheritance_RuleERC2980.sol](./doc/surya/surya_inheritance/surya_inheritance_RuleERC2980.sol.png) @@ -530,6 +588,8 @@ Restriction codes: | `CODE_ADDRESS_TO_IS_FROZEN` | 61 | Recipient is frozen | | `CODE_ADDRESS_SPENDER_IS_FROZEN` | 62 | Spender is frozen | | `CODE_ADDRESS_TO_NOT_WHITELISTED` | 63 | Recipient is not whitelisted | +| `CODE_MINT_NOT_ALLOWED` | 64 | Minting is disabled (`allowMint == false`) | +| `CODE_BURN_NOT_ALLOWED` | 65 | Burning is disabled (`allowBurn == false`) | **Deviation from spec**: the ERC-2980 `Whitelistable` / `Freezable` example interfaces define single-address management functions that return `bool` and do not revert on duplicates or missing entries. This implementation reverts on invalid single-item operations, consistent with the codebase convention. Batch operations remain non-reverting. @@ -569,7 +629,15 @@ The operator deploys `RuleMaxTotalSupply` with `setMaxTotalSupply(1_000_000)` an #### Identity registry -If an identity registry address is set, this rule checks `isVerified` for the sender, recipient, and spender (for `transferFrom`). Zero addresses are ignored, and burns (`to == address(0)`) are always allowed so non‑verified holders can burn. +**ERC-3643 conformant: only the RECEIVER is verified.** The specification mandates exactly one identity check — *"The receiver MUST be whitelisted on the Identity Registry and verified"* — and states that `transferFrom` "works the same way", that `mint` "only require[s] the receiver", and that `burn` "bypasses all checks on eligibility". The **sender**, the **spender** and the **minter** are therefore **not** verified by default. + +Checking the sender is deliberately avoided: ERC-3643 screens only the receiver precisely so that an investor whose identity lapses can still **exit their position** by sending to a verified counterparty. Screening the sender would trap them — unable to receive *and* unable to send. + +Stricter screening is available as an **explicit opt-in**, never a silent default: +- `checkSender` — also verify the sender (stricter than ERC-3643). +- `checkSpender` — also verify the spender on `transferFrom` (stricter than ERC-3643). Mint and burn stay exempt regardless. + +Constructors: `RuleIdentityRegistry(address admin, address identityRegistry, bool checkSender, bool checkSpender)` — pass `false, false` for the conformant default. Both flags are settable afterwards via `setCheckSender(bool)` / `setCheckSpender(bool)`. ![surya_inheritance_RuleIdentityRegistry.sol](./doc/surya/surya_inheritance/surya_inheritance_RuleIdentityRegistry.sol.png) @@ -579,7 +647,7 @@ The operator calls `setIdentityRegistry(registry)`. The issuer attempts a transf ### Read-Write (Operation) rule -For the moment, there is only one operation rule available: ConditionalTransferLight. +There are three operation rules available: `RuleConditionalTransferLight`, `RuleConditionalTransferLightMultiToken`, and `RuleMintAllowance`. #### Conditional transfer (light) @@ -591,6 +659,28 @@ This rule requires that transfers must be approved by an operator before being e An operator calls `approveTransfer(from, to, value)`. The compliance manager binds exactly one token with `bindToken(token)`; attempting to bind a second token reverts. The token calls `detectTransferRestriction` (passes) and later `transferred` to consume the approval. Without approval, `detectTransferRestriction` returns code 46 and the transfer is rejected. The operator can revoke with `cancelTransferApproval`. To migrate to a different token, the compliance manager must first call `unbindToken` before binding the new one. +#### Mint allowance + +This rule enforces a per-minter mint quota for one bound RuleEngine/token at a time. An operator sets the number of tokens each minter address is allowed to mint via `setMintAllowance(minter, amount)`. Every successful mint reduces the minter's remaining quota. The operator can adjust quotas at any time with `increaseMintAllowance` / `decreaseMintAllowance`. Regular transfers and burns are not restricted. + +Compatibility warning: `RuleMintAllowance` does not enforce quotas for a token that only calls the standard ERC-3643 3-arg compliance functions. It requires the CMTAT/RuleEngine spender-aware path so the minter address is passed as `spender`. + +For the same reason, it does not advertise the full ERC-3643 `ICompliance` interface through ERC-165; the 3-arg callbacks alone cannot enforce the mint quota. + +> ⚠️ **`canTransfer` / `detectTransferRestriction` are not authoritative for this rule** — they are hardcoded to "allowed" because the 3-arg signature has no minter identity, so they disagree with enforcement. Pre-flight a mint with the spender-aware view `canTransferFrom(minter, address(0), to, value)` (or `detectTransferRestrictionFrom`). See [RuleMintAllowance.md](./doc/technical/RuleMintAllowance.md#eligibility-views-which-one-is-authoritative). + +**Usage scenario** + +The compliance manager binds the rule to the RuleEngine with `bindToken(ruleEngine)`. Attempting to bind a second RuleEngine/token reverts until the current binding is removed with `unbindToken`. The operator assigns `setMintAllowance(alice, 100_000e18)`. Alice's mints deduct from her quota through `transferred(alice, address(0), recipient, amount)`; once exhausted, further mints revert with code 70 until the operator increases the quota. + +#### Conditional transfer (light, multi-token) + +This variant scopes approvals by token address. It hashes `(token, from, to, value)` and supports multiple bound tokens in a single rule instance. Each successful transfer consumes one approval in the calling token namespace. Mints (`from == address(0)`) and burns (`to == address(0)`) remain exempt. + +**Usage scenario** + +An operator calls `approveTransfer(tokenA, from, to, value)` for `tokenA`. A transfer on `tokenA` succeeds and consumes the approval. The same `(from, to, value)` transfer on `tokenB` is still rejected until separately approved with `approveTransfer(tokenB, from, to, value)`. + ## Access Control The module `AccessControlModuleStandalone` implements RBAC access control by inheriting from OpenZeppelin's `AccessControlEnumerable`. @@ -618,8 +708,9 @@ See also [docs.openzeppelin.com - AccessControl](https://docs.openzeppelin.com/c | `ADDRESS_LIST_REMOVE_ROLE` | `0x1b94c92b564251ed6b49246d9a82eb7a486b6490f3b3a3bf3b28d2e99801f3ec` | `removeAddress`, `removeAddresses` (RuleWhitelist, RuleBlacklist) | | `SANCTIONLIST_ROLE` | `0x30842281ac34bdc7d568c7ab276f84ba6fc1a1de1ae858b0afd35e716fb0650d` | `setSanctionListOracle`, `clearSanctionListOracle` (RuleSanctionsList) | | `RULES_MANAGEMENT_ROLE` | `0xea5f4eb72290e50c32abd6c23e45de3d8300b3286e1cbc2e293114b92e034e5e` | `setRules`, `clearRules`, `addRule`, `removeRule` (RuleWhitelistWrapper) | -| `OPERATOR_ROLE` | `0x97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b929` | `approveTransfer`, `cancelTransferApproval` (RuleConditionalTransferLight) | -| `COMPLIANCE_MANAGER_ROLE` | `0xe5c50d0927e06141e032cb9a67e1d7092dc85c0b0825191f7e1cede600028568` | `bindToken`, `unbindToken` (RuleConditionalTransferLight) | +| `OPERATOR_ROLE` | `0x97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b929` | `approveTransfer`, `cancelTransferApproval` (RuleConditionalTransferLight / RuleConditionalTransferLightMultiToken) | +| `COMPLIANCE_MANAGER_ROLE` | `0xe5c50d0927e06141e032cb9a67e1d7092dc85c0b0825191f7e1cede600028568` | `bindToken`, `unbindToken` (RuleConditionalTransferLight / RuleConditionalTransferLightMultiToken / RuleMintAllowance) | +| `ALLOWANCE_OPERATOR_ROLE` | `0x86a2482724302deea267bc1ca14032806c318aeaf8d1e0d445a6fb7e7c997beb` | `setMintAllowance`, `increaseMintAllowance`, `decreaseMintAllowance` (RuleMintAllowance) | | `WHITELIST_ADD_ROLE` | `0x77c0b4c0975a0b0417d8ce295502737b95fee8923755fed0cce952907a1861ed` | `addWhitelistAddress`, `addWhitelistAddresses` (RuleERC2980) | | `WHITELIST_REMOVE_ROLE` | `0xf4d11a530c5b90f459c6ab1e335d3d77156b8ff3093308e4fca6d100ee87ade9` | `removeWhitelistAddress`, `removeWhitelistAddresses` (RuleERC2980) | | `FROZENLIST_ADD_ROLE` | `0xc52c49807a071974b9260f4b553ee09bd9fd85f687d8d4cc3232de7104ff7835` | `addFrozenlistAddress`, `addFrozenlistAddresses` (RuleERC2980) | @@ -637,231 +728,60 @@ For simpler ownership-based control, `Ownable2Step` variants (two-step ownership - `RuleMaxTotalSupplyOwnable2Step` - `RuleERC2980Ownable2Step` - `RuleConditionalTransferLightOwnable2Step` +- `RuleConditionalTransferLightMultiTokenOwnable2Step` +- `RuleMintAllowanceOwnable2Step` `RuleConditionalTransferLightOwnable2Step` now grants approval and execution permissions exclusively to the owner. All `Ownable2Step` variants enforce access using OpenZeppelin's `onlyOwner` modifier. +All `Ownable2Step` variants also advertise ERC-165 support for `IERC165` (`0x01ffc9a7`), ERC-173 ownership (`0x7f5828d0`), and Ownable2Step handover (`0x9ab669ef`). ### Address List -Common access control between `blacklistRule`and `WhitelistRule` +Common access control between the blacklist rule and whitelist rule. These roles are listed above in the Role Summary table. - ## Toolchains and Usage -### Configuration - -Here are the settings for [Hardhat](https://hardhat.org) and [Foundry](https://getfoundry.sh). - -- `hardhat.config.js` - - - Solidity [v0.8.34](https://docs.soliditylang.org/en/v0.8.34/) - - EVM version: Prague (Pectra upgrade) - - Optimizer: true, 200 runs - -- `foundry.toml` - - - Solidity [v0.8.34](https://docs.soliditylang.org/en/v0.8.34/) - - EVM version: Prague (Pectra upgrade) - - Optimizer: true, 200 runs - -- Library - - - Foundry [v1.5.0](https://github.com/foundry-rs/foundry) - - - Forge std [v1.12.0](https://github.com/foundry-rs/forge-std/releases/tag/v1.12.0 ) - - - OpenZeppelin Contracts (submodule) [v5.6.1](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.6.1) - - - OpenZeppelin Contracts Upgradeable (submodule) [v5.6.1](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/releases/tag/v5.6.1) +This repository is developed and tested with [Foundry](https://book.getfoundry.sh); a Hardhat config is also present for compilation and a small smoke test. Build settings (`foundry.toml` / `hardhat.config.js`): solc `v0.8.34`, EVM `Prague`, optimizer on (200 runs). - - CMTAT [v3.2.0](https://github.com/CMTA/CMTAT/releases/tag/v3.2.0) +### Main commands - - RuleEngine [v3.0.0-rc2](https://github.com/CMTA/RuleEngine/releases/tag/v3.0.0-rc2) +| Task | Command | +| --- | --- | +| Install / update submodules | `forge install` · `forge update` | +| Build | `forge build` | +| Contract sizes | `forge compile --sizes` | +| Run all tests | `forge test` | +| Run one test | `forge test --match-contract --match-test ` | +| Gas report | `forge test --gas-report` | +| Gas snapshot | `forge snapshot` (check only: `forge snapshot --check`) | +| Coverage | `forge coverage` | +| Coverage report ([`doc/coverage`](./doc/coverage/)) | `forge coverage --no-match-coverage "(script\|mocks\|test)" --report lcov && genhtml lcov.info --branch-coverage --prefix "$PWD/" --output-dir coverage` | +| Invariant suite only | `forge test --match-path "test/invariant/*"` | +| Format | `forge fmt` | +| Deploy a script | `forge script script/.s.sol --rpc-url --account ` | -### Toolchain installation +### Invariant testing -This repository is primarily developed and tested with [Foundry](https://book.getfoundry.sh), a smart contract development toolchain. +The two **stateful (operation) rules** — `RuleConditionalTransferLight` and `RuleMintAllowance` — are covered by a handler-driven `StdInvariant` suite in [`test/invariant/`](./test/invariant/), which fuzzes long randomly-ordered call sequences and re-checks four invariants after every step (8 192 calls each, `fail_on_revert = true`): -Hardhat configuration is also present to support contract compilation and a small smoke test with Hardhat. +| Invariant | Asserts | +| --- | --- | +| `invariant_approvalConservation` | `totalApproved − totalCancelled − totalExecuted == Σ approvalCounts` — approvals are never double-spent or lost | +| `invariant_noApprovalExceedsTotalRecorded` | `Σ approvalCounts ≤ totalApproved` | +| `invariant_allowanceMatchesGhost` | the on-chain mint quota exactly matches an independently-computed ghost mirror, after any interleaving | +| `invariant_mintedNeverExceedsCredited` | `Σ minted ≤ Σ credited` | -To install the Foundry suite, please refer to the official instructions in the [Foundry book](https://book.getfoundry.sh/getting-started/installation). +Both suites are **mutation-verified**: injecting an approval double-spend or an off-by-one quota deduction makes them fail. Validation rules are read-only and hold no per-transfer state, so they are covered by unit and fuzz tests instead. -### Initialization +Full details — handler architecture, ghost variables, the negative controls, the coverage map against the threat-model invariants, and how to add a new one — are in **[doc/technical/INVARIANT_TESTS.md](./doc/technical/INVARIANT_TESTS.md)**. -You must first initialize the submodules, with - -``` -forge install -``` - -See also the command's [documentation](https://book.getfoundry.sh/reference/forge/forge-install). - -Later you can update all the submodules with: - -``` -forge update -``` - -See also the command's [documentation](https://book.getfoundry.sh/reference/forge/forge-update). - -### Compilation - -The official documentation is available in the Foundry [website](https://book.getfoundry.sh/reference/forge/build-commands) - -``` - forge build -``` - -Hardhat compilation (optional): - -```bash -npm run hardhat:compile -``` - -### Contract size - -```bash - forge compile --sizes -``` - -### Testing - -You can run the tests with - -```bash -forge test -``` - -Hardhat smoke test (optional): - -```bash -npm run hardhat:test:smoke -``` - -To run a specific test, use - -```bash -forge test --match-contract --match-test -``` - -- For `RuleConditionalTransferLight` fuzz/integration tests, note that mint and burn paths (`from == address(0)` or `to == address(0)`) are intentionally exempt from approval consumption. -- Ownable2Step variants also include dedicated tests for ownership transfer and manager-only functions (IdentityRegistry, MaxTotalSupply, SanctionsList). -- Coverage-focused tests also target deployment wrappers and operation-rule overloads (`created`, `destroyed`, spender-aware `transferred`) to improve line/function coverage in `src/rules/operation` and `src/rules/validation/deployment`. - -Generate gas report - -```bash -forge test --gas-report -``` - -See also the test framework's [official documentation](https://book.getfoundry.sh/forge/tests), and that of the [test commands](https://book.getfoundry.sh/reference/forge/test-commands). - -### Gas Benchmarks - -Gas usage is tracked in two complementary files: - -- **`.gas-snapshot`** — machine-generated file produced by `forge snapshot`. It records the gas cost of every test function and is checked into the repository so that gas regressions are visible in diffs. Regenerate it with: - - ```bash - forge snapshot - ``` - - To check for regressions against the committed snapshot without overwriting it: - - ```bash - forge snapshot --check - ``` - -- **`doc/GAS.md`** — human-readable summary of key operation costs (e.g. `addAddress`, `detectTransferRestriction`) with the date of the last measurement. Update it manually after running `forge snapshot` when behaviour or gas costs change. - -### Coverage - -![coverage](./doc/coverage/coverage.png) - -A code coverage is available in [index.html](./doc/coverage/coverage/index.html). - -* Perform a code coverage - -``` -forge coverage -``` - -* Generate LCOV report - -``` -forge coverage --report lcov -``` - -- Generate `index.html` - -```bash -forge coverage --no-match-coverage "(script|mocks|test)" --report lcov && genhtml lcov.info --branch-coverage --prefix "$PWD/" --output-dir coverage -``` - -See [Solidity Coverage in VS Code with Foundry](https://mirror.xyz/devanon.eth/RrDvKPnlD-pmpuW7hQeR5wWdVjklrpOgPCOA-PJkWFU) & [Foundry forge coverage](https://www.rareskills.io/post/foundry-forge-coverage) - -### Other - -Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust. - -Foundry consists of: - -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. - -#### Documentation - -https://book.getfoundry.sh/ - - -#### Format - -```shell -$ forge fmt -``` - -#### Gas Snapshots - -```shell -$ forge snapshot -``` - -#### Anvil - -```shell -$ anvil -``` - -#### Deploy - -> **Warning — private key security** -> Passing `--private-key` directly on the command line is **not recommended** in production: the key is visible in your shell history and to any process that can read `/proc`. Prefer hardware wallets (`--ledger`, `--trezor`), encrypted keystores (`--account `), or environment-variable signers. See [Foundry best practices](https://www.getfoundry.sh/best-practices) for details. - -```shell -$ forge script script/DeployCMTATWithWhitelist.s.sol --rpc-url --private-key -$ forge script script/DeployCMTATWithBlacklist.s.sol --rpc-url --private-key -$ forge script script/DeployCMTATWithBlacklistAndSanctionsList.s.sol --rpc-url --private-key -``` - -#### Cast - -```shell -$ cast -``` - -#### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` +Deployment scripts: `script/DeployCMTATWithWhitelist.s.sol`, `script/DeployCMTATWithBlacklist.s.sol`, `script/DeployCMTATWithBlacklistAndSanctionsList.s.sol`. +> **Deployment key security:** avoid passing `--private-key` on the command line (visible in shell history and to any process that can read `/proc`). Prefer hardware wallets (`--ledger`, `--trezor`) or encrypted keystores (`--account `). See [Foundry best practices](https://www.getfoundry.sh/best-practices). +For the full toolchain guide — dependency versions, Hardhat commands, HTML coverage generation, the gas-benchmark workflow, and the generic Forge / Cast / Anvil / Chisel reference — see **[doc/FOUNDRY.md](./doc/FOUNDRY.md)** and the [Foundry book](https://book.getfoundry.sh/). ## API @@ -1679,10 +1599,68 @@ Returns the number of approvals for the transfer hash. ## Security +### Manual Threat Model & Review (v0.4.0) + +The published report is [**`CLAUDE_AUDIT.md`**](./doc/security/audits/tools/v0.4.0/claude-audit/CLAUDE_AUDIT.md) — findings, invariant verification, access-control verification, what was remediated, and the open improvement backlog. It is backed by the working deliverables at the repository root: + +| Document | Contents | +|---|---| +| [`CLAUDE_AUDIT.md`](./doc/security/audits/tools/v0.4.0/claude-audit/CLAUDE_AUDIT.md) | **The audit report.** Findings, invariant + access-control verification, remediation record, open backlog | +| [`THREAT_MODEL.md`](./THREAT_MODEL.md) | Trust model and actors, 30 catalogued threats with IDs, data-flow diagrams, 12 invariants, reachable privileged surface | +| [`RESULT.md`](./RESULT.md) | Findings, invariant and access-control verification, and an explicit disposition for every threat ID | +| [`TEST_IMPROVEMENT.md`](./TEST_IMPROVEMENT.md) | Test-gap analysis and the deferred test backlog | + +**Outcome: 0 Critical, 0 High, 0 Medium, 2 Low, 8 Informational.** Two hypotheses that would have been High were specifically probed and cleared: an ERC-2771 forwarder cannot impersonate a bound token (the operation rules deliberately do not inherit `ERC2771Context`), and the hand-rolled keccak preimage in `_transferHash` is injective. + +| ID | Severity | Summary | +|---|---|---| +| F-1 | Low | `RuleIdentityRegistry` screens the minter as `spender` on mint, unlike its three sibling allowlist rules, so issuance halts unless the minter is itself identity-verified. Fail-closed; no bypass | +| F-4 | Low | `RuleConditionalTransferLightMultiToken` stores approvals under the caller-supplied `token` but consumes them under `msg.sender`. Behind a shared `RuleEngine` this strands token-keyed approvals and collapses per-token isolation | +| F-2, F-3, F-5, F-7, F-8, F-9, F-10, F-14 | Info | Max-supply views panic on overflow; `approveAndTransferIfAllowed` is direct-binding-only; the wrapper does not interface-check child rules; `RuleMintAllowance.canTransfer` is not authoritative; multi-token `detectTransferRestriction` depends on `msg.sender`; `unbindToken` leaves stale state; documentation drift | + +Proofs live in [`test/ThreatModel/ThreatModelTests.t.sol`](./test/ThreatModel/ThreatModelTests.t.sol) (18 tests: 15 unit/integration, 3 fuzz). + ### Automated Analysis -Latest tool outputs for this release cycle (including feedback documents) are available in [`doc/security/audits/tools/v0.3.0/`](./doc/security/audits/tools/v0.3.0/). -`v0.3.0` cleanup: removed unused `RuleConditionalTransferLight_TransferFailed` custom error after SafeERC20 migration. +See the consolidated [Audit & Security-Analysis Overview](./doc/security/audits/AUDIT_OVERVIEW.md) for the full index and triage. Latest tool outputs (including feedback documents) are in [`doc/security/audits/tools/v0.4.0/`](./doc/security/audits/tools/v0.4.0/). + +Commands used for `v0.4.0` (mocks excluded): + +```bash +slither . --checklist --filter-paths "node_modules,lib,test,forge-std,mocks" \ + > doc/security/audits/tools/v0.4.0/slither-report.md +aderyn -x mocks --output doc/security/audits/tools/v0.4.0/aderyn-report.md +``` + +#### Aderyn (v0.4.0) + +Static analysis with [Aderyn](https://github.com/Cyfrin/aderyn) 0.6.5, re-run **2026-07-14** after the security remediation. Full report and feedback in [`doc/security/audits/tools/v0.4.0/`](./doc/security/audits/tools/v0.4.0/). **No High/Medium issues; nothing to fix** — all 9 Low findings are by-design or false positives (see [feedback](./doc/security/audits/tools/v0.4.0/aderyn-report-feedback.md)). The run initially reported 10: an `Unused Import` (dead `RuleTransferValidation` import in the two `RuleSpenderWhitelist` deployment files) was a genuine cosmetic defect and has been **fixed**. + +| ID | Title | Instances | Verdict | +|---|---|---|---| +| L-1 | Centralization Risk | 68 | By design (regulated token issuer model) | +| L-2 | Unspecific Solidity Pragma | 63 | By design (`^0.8.20` library; project pins solc 0.8.34) | +| L-3 | Address State Variable Set Without Checks | 1 | False positive — zero-check enforced at public `setSanctionListOracle` | +| L-4 | PUSH0 Opcode | 64 | By design — project targets Prague EVM | +| L-5 | Modifier Invoked Only Once | 2 | By design — template method pattern | +| L-6 | Empty Block | 61 | By design — `_authorize*()` hooks + required interface no-ops | +| L-7 | Loop Contains `require`/`revert` | 3 | **By design — recommendation rejected.** Batch adds revert on `address(0)` on purpose: skipping it made the emitted event name the sentinel as a set member | +| L-8 | Costly operations inside loop | 7 | By design — `EnumerableSet` requires one `SSTORE` per element | +| L-9 | Unchecked Return | 13 | Mixed — majority false positives; constructor `_grantRole` intentional | +| — | Unused Import | 0 | **Fixed** during this run (was 2) | + +#### Slither (v0.4.0) + +Static analysis with [Slither](https://github.com/crytic/slither) 0.11.5, re-run **2026-07-14** after the security remediation (tally unchanged from the previous run). Full report and feedback in [`doc/security/audits/tools/v0.4.0/`](./doc/security/audits/tools/v0.4.0/). **Nothing to fix** — the two High `arbitrary-send-erc20` hits are false positives (approval-gated, allowance-checked compliance flow); see [feedback](./doc/security/audits/tools/v0.4.0/slither-report-feedback.md). + +| Category | Severity | Instances | Verdict | +|---|---|---|---| +| arbitrary-send-erc20 | High | 2 | False positive — `from` guarded by `onlyTransferApprover`, recorded approval, allowance check, bound token (light + multi-token) | +| unused-return | Medium | 6 | False positive — existence pre-checked at public layer before internal helper | +| calls-loop | Low | 16 | By design — wrapper must query each child rule; child rules are read-only | +| assembly | Informational | 2 | By design — memory-safe hash in `_transferHash` (light + multi-token) | +| naming-convention | Informational | 2 | By design — parameter names match ERC-2980 spec | +| unused-state | Informational | 8 | False positive — `RuleNFTAdapter` constants used in base dispatch (per-contract analysis limitation) | #### Aderyn (v0.3.0) diff --git a/doc/ERCSpecification/eip-1.md b/doc/ERCSpecification/eip-1.md new file mode 100644 index 0000000..b3ee5d4 --- /dev/null +++ b/doc/ERCSpecification/eip-1.md @@ -0,0 +1,619 @@ +--- +eip: 1 +title: EIP Purpose and Guidelines +status: Living +type: Meta +author: Martin Becze , Hudson Jameson , et al. +created: 2015-10-27 +--- + +## What is an EIP? + +EIP stands for Ethereum Improvement Proposal. An EIP is a design document providing information to the Ethereum community, or describing a new feature for Ethereum or its processes or environment. The EIP should provide a concise technical specification of the feature and a rationale for the feature. The EIP author is responsible for building consensus within the community and documenting dissenting opinions. + +## EIP Rationale + +We intend EIPs to be the primary mechanisms for proposing new features, for collecting community technical input on an issue, and for documenting the design decisions that have gone into Ethereum. Because the EIPs are maintained as text files in a versioned repository, their revision history is the historical record of the feature proposal. + +For Ethereum implementers, EIPs are a convenient way to track the progress of their implementation. Ideally each implementation maintainer would list the EIPs that they have implemented. This will give end users a convenient way to know the current status of a given implementation or library. + +## EIP Types + +There are three types of EIP: + +- A **Standards Track EIP** describes any change that affects most or all Ethereum implementations, such as—a change to the network protocol, a change in block or transaction validity rules, proposed application standards/conventions, or any change or addition that affects the interoperability of applications using Ethereum. Standards Track EIPs consist of three parts—a design document, an implementation, and (if warranted) an update to the [formal specification](https://github.com/ethereum/yellowpaper). Furthermore, Standards Track EIPs can be broken down into the following categories: + - **Core**: improvements requiring a consensus fork (e.g. [EIP-5](./eip-5.md), [EIP-101](./eip-101.md)), as well as changes that are not necessarily consensus critical but may be relevant to [“core dev” discussions](https://github.com/ethereum/pm) (for example, [EIP-90], and the miner/node strategy changes 2, 3, and 4 of [EIP-86](./eip-86.md)). + - **Networking**: includes improvements around [devp2p](https://github.com/ethereum/devp2p/blob/readme-spec-links/rlpx.md) ([EIP-8](./eip-8.md)) and [Light Ethereum Subprotocol](https://ethereum.org/en/developers/docs/nodes-and-clients/#light-node), as well as proposed improvements to network protocol specifications of [whisper](https://github.com/ethereum/go-ethereum/issues/16013#issuecomment-364639309) and [swarm](https://github.com/ethereum/go-ethereum/pull/2959). + - **Interface**: includes improvements around language-level standards like method names ([EIP-6](./eip-6.md)) and [contract ABIs](https://docs.soliditylang.org/en/develop/abi-spec.html). + - **ERC**: application-level standards and conventions, including contract standards such as token standards ([ERC-20](./eip-20.md)), name registries ([ERC-137](./eip-137.md)), URI schemes, library/package formats, and wallet formats. + +- A **Meta EIP** describes a process surrounding Ethereum or proposes a change to (or an event in) a process. Process EIPs are like Standards Track EIPs but apply to areas other than the Ethereum protocol itself. They may propose an implementation, but not to Ethereum's codebase; they often require community consensus; unlike Informational EIPs, they are more than recommendations, and users are typically not free to ignore them. Examples include procedures, guidelines, changes to the decision-making process, and changes to the tools or environment used in Ethereum development. Any meta-EIP is also considered a Process EIP. + +- An **Informational EIP** describes an Ethereum design issue, or provides general guidelines or information to the Ethereum community, but does not propose a new feature. Informational EIPs do not necessarily represent Ethereum community consensus or a recommendation, so users and implementers are free to ignore Informational EIPs or follow their advice. + +It is highly recommended that a single EIP contain a single key proposal or new idea. The more focused the EIP, the more successful it tends to be. A change to one client doesn't require an EIP; a change that affects multiple clients, or defines a standard for multiple apps to use, does. + +An EIP must meet certain minimum criteria. It must be a clear and complete description of the proposed enhancement. The enhancement must represent a net improvement. The proposed implementation, if applicable, must be solid and must not complicate the protocol unduly. + +### Special requirements for Core EIPs + +If a **Core** EIP mentions or proposes changes to the EVM (Ethereum Virtual Machine), it should refer to the instructions by their mnemonics and define the opcodes of those mnemonics at least once. A preferred way is the following: + +``` +REVERT (0xfe) +``` + +## EIP Work Flow + +### Shepherding an EIP + +Parties involved in the process are you, the champion or *EIP author*, the [*EIP editors*](#eip-editors), and the [*Ethereum Core Developers*](https://github.com/ethereum/pm). + +Before you begin writing a formal EIP, you should vet your idea. Ask the Ethereum community first if an idea is original to avoid wasting time on something that will be rejected based on prior research. It is thus recommended to open a discussion thread on [the Ethereum Magicians forum](https://ethereum-magicians.org/) to do this. + +Once the idea has been vetted, your next responsibility will be to present (by means of an EIP) the idea to the reviewers and all interested parties, invite editors, developers, and the community to give feedback on the aforementioned channels. You should try and gauge whether the interest in your EIP is commensurate with both the work involved in implementing it and how many parties will have to conform to it. For example, the work required for implementing a Core EIP will be much greater than for an ERC and the EIP will need sufficient interest from the Ethereum client teams. Negative community feedback will be taken into consideration and may prevent your EIP from moving past the Draft stage. + +### Core EIPs + +For Core EIPs, given that they require client implementations to be considered **Final** (see "EIPs Process" below), you will need to either provide an implementation for clients or convince clients to implement your EIP. + +The best way to get client implementers to review your EIP is to present it on an AllCoreDevs call. You can request to do so by posting a comment linking your EIP on an [AllCoreDevs agenda GitHub Issue](https://github.com/ethereum/pm/issues). + +The AllCoreDevs call serves as a way for client implementers to do three things. First, to discuss the technical merits of EIPs. Second, to gauge what other clients will be implementing. Third, to coordinate EIP implementation for network upgrades. + +These calls generally result in a "rough consensus" around what EIPs should be implemented. This "rough consensus" rests on the assumptions that EIPs are not contentious enough to cause a network split and that they are technically sound. + +:warning: The EIPs process and AllCoreDevs call were not designed to address contentious non-technical issues, but, due to the lack of other ways to address these, often end up entangled in them. This puts the burden on client implementers to try and gauge community sentiment, which hinders the technical coordination function of EIPs and AllCoreDevs calls. If you are shepherding an EIP, you can make the process of building community consensus easier by making sure that [the Ethereum Magicians forum](https://ethereum-magicians.org/) thread for your EIP includes or links to as much of the community discussion as possible and that various stakeholders are well-represented. + +*In short, your role as the champion is to write the EIP using the style and format described below, shepherd the discussions in the appropriate forums, and build community consensus around the idea.* + +### EIP Process + +The following is the standardization process for all EIPs in all tracks: + +![EIP Status Diagram](../assets/eip-1/EIP-process-update.jpg) + +**Idea** - An idea that is pre-draft. This is not tracked within the EIP Repository. + +**Draft** - The first formally tracked stage of an EIP in development. An EIP is merged by an EIP Editor into the EIP repository when properly formatted. + +**Review** - An EIP Author marks an EIP as ready for and requesting Peer Review. + +**Last Call** - This is the final review window for an EIP before it is moved to `Final`. An EIP enters `Last Call` when the specification is stable and the author opens a PR with a review end date (`last-call-deadline`), typically 14 days later. + +If this period results in necessary normative changes it will revert the EIP to `Review`. + +**Final** - This EIP represents the final standard. A Final EIP exists in a state of finality and should only be updated to correct errata and add non-normative clarifications. + +A PR moving an EIP from Last Call to Final SHOULD contain no changes other than the status update. Any content or editorial proposed change SHOULD be separate from this status-updating PR and committed prior to it. + +**Stagnant** - Any EIP in `Draft` or `Review` or `Last Call` if inactive for a period of 6 months or greater is moved to `Stagnant`. An EIP may be resurrected from this state by Authors or EIP Editors through moving it back to `Draft` or its earlier status. If not resurrected, a proposal may stay forever in this status. + +>*EIP Authors are notified of any algorithmic change to the status of their EIP* + +**Withdrawn** - The EIP Author(s) have withdrawn the proposed EIP. This state has finality and can no longer be resurrected using this EIP number. If the idea is pursued at later date it is considered a new proposal. + +**Living** - A special status for EIPs that are designed to be continually updated and not reach a state of finality. This includes most notably EIP-1. + +## What belongs in a successful EIP? + +Each EIP should have the following parts: + +- Preamble - RFC 822 style headers containing metadata about the EIP, including the EIP number, a short descriptive title (limited to a maximum of 44 characters), a description (limited to a maximum of 140 characters), and the author details. Irrespective of the category, the title and description should not include EIP number. See [below](./eip-1.md#eip-header-preamble) for details. +- Abstract - Abstract is a multi-sentence (short paragraph) technical summary. This should be a very terse and human-readable version of the specification section. Someone should be able to read only the abstract to get the gist of what this specification does. +- Motivation *(optional)* - A motivation section is critical for EIPs that want to change the Ethereum protocol. It should clearly explain why the existing protocol specification is inadequate to address the problem that the EIP solves. This section may be omitted if the motivation is evident. +- Specification - The technical specification should describe the syntax and semantics of any new feature. The specification should be detailed enough to allow competing, interoperable implementations for any of the current Ethereum platforms (besu, erigon, ethereumjs, go-ethereum, nethermind, or others). +- Rationale - The rationale fleshes out the specification by describing what motivated the design and why particular design decisions were made. It should describe alternate designs that were considered and related work, e.g. how the feature is supported in other languages. The rationale should discuss important objections or concerns raised during discussion around the EIP. +- Backwards Compatibility *(optional)* - All EIPs that introduce backwards incompatibilities must include a section describing these incompatibilities and their consequences. The EIP must explain how the author proposes to deal with these incompatibilities. This section may be omitted if the proposal does not introduce any backwards incompatibilities, but this section must be included if backward incompatibilities exist. +- Test Cases *(optional)* - Test cases for an implementation are mandatory for EIPs that are affecting consensus changes. Tests should either be inlined in the EIP as data (such as input/expected output pairs) or included in `../assets/eip-###/`. This section may be omitted for non-Core proposals. +- Reference Implementation *(optional)* - An optional section that contains a reference/example implementation that people can use to assist in understanding or implementing this specification. This section may be omitted for all EIPs. +- Security Considerations - All EIPs must contain a section that discusses the security implications/considerations relevant to the proposed change. Include information that might be important for security discussions, surfaces risks and can be used throughout the life-cycle of the proposal. E.g. include security-relevant design decisions, concerns, important discussions, implementation-specific guidance and pitfalls, an outline of threats and risks and how they are being addressed. EIP submissions missing the "Security Considerations" section will be rejected. An EIP cannot proceed to status "Final" without a Security Considerations discussion deemed sufficient by the reviewers. +- Copyright Waiver - All EIPs must be in the public domain. The copyright waiver MUST link to the license file and use the following wording: `Copyright and related rights waived via [CC0](../LICENSE.md).` + +## EIP Formats and Templates + +EIPs should be written in [markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) format. There is a [template](https://github.com/ethereum/EIPs/blob/master/eip-template.md) to follow. + +## EIP Header Preamble + +Each EIP must begin with an [RFC 822](https://www.ietf.org/rfc/rfc822.txt) style header preamble, preceded and followed by three hyphens (`---`). This header is also termed ["front matter" by Jekyll](https://jekyllrb.com/docs/front-matter/). The headers must appear in the following order. + +`eip`: *EIP number* + +`title`: *The EIP title is a few words, not a complete sentence* + +`description`: *Description is one full (short) sentence* + +`author`: *The list of the author's or authors' name(s) and/or username(s), or name(s) and email(s). Details are below.* + +`discussions-to`: *The url pointing to the official discussion thread* + +`status`: *Draft, Review, Last Call, Final, Stagnant, Withdrawn, Living* + +`last-call-deadline`: *The date last call period ends on* (Optional field, only needed when status is `Last Call`) + +`type`: *One of `Standards Track`, `Meta`, or `Informational`* + +`category`: *One of `Core`, `Networking`, `Interface`, or `ERC`* (Optional field, only needed for `Standards Track` EIPs) + +`created`: *Date the EIP was created on* + +`requires`: *EIP number(s)* (Optional field) + +`withdrawal-reason`: *A sentence explaining why the EIP was withdrawn.* (Optional field, only needed when status is `Withdrawn`) + +Headers that permit lists must separate elements with commas. + +Headers requiring dates will always do so in the format of ISO 8601 (yyyy-mm-dd). + +### `author` header + +The `author` header lists the names, email addresses or usernames of the authors/owners of the EIP. Those who prefer anonymity may use a username only, or a first name and a username. The format of the `author` header value must be: + +> Random J. User <address@dom.ain> + +or + +> Random J. User (@username) + +or + +> Random J. User (@username) <address@dom.ain> + +if the email address and/or GitHub username is included, and + +> Random J. User + +if neither the email address nor the GitHub username are given. + +At least one author must use a GitHub username, in order to get notified on change requests and have the capability to approve or reject them. + +### `discussions-to` header + +While an EIP is a draft, a `discussions-to` header will indicate the URL where the EIP is being discussed. + +The preferred discussion URL is a topic on [Ethereum Magicians](https://ethereum-magicians.org/). The URL cannot point to Github pull requests, any URL which is ephemeral, and any URL which can get locked over time (i.e. Reddit topics). + +### `type` header + +The `type` header specifies the type of EIP: Standards Track, Meta, or Informational. If the track is Standards please include the subcategory (core, networking, interface, or ERC). + +### `category` header + +The `category` header specifies the EIP's category. This is required for standards-track EIPs only. + +### `created` header + +The `created` header records the date that the EIP was assigned a number. Both headers should be in yyyy-mm-dd format, e.g. 2001-08-14. + +### `requires` header + +EIPs may have a `requires` header, indicating the EIP numbers that this EIP depends on. If such a dependency exists, this field is required. + +A `requires` dependency is created when the current EIP cannot be understood or implemented without a concept or technical element from another EIP. Merely mentioning another EIP does not necessarily create such a dependency. + +## Linking to External Resources + +Other than the specific exceptions listed below, links to external resources **SHOULD NOT** be included. External resources may disappear, move, or change unexpectedly. + +The process governing permitted external resources is described in [EIP-5757](./eip-5757.md). + +### Execution Client Specifications + +Links to the Ethereum Execution Client Specifications may be included using normal markdown syntax, such as: + +```markdown +[Ethereum Execution Client Specifications](https://github.com/ethereum/execution-specs/blob/9a1f22311f517401fed6c939a159b55600c454af/README.md) +``` + +Which renders to: + +[Ethereum Execution Client Specifications](https://github.com/ethereum/execution-specs/blob/9a1f22311f517401fed6c939a159b55600c454af/README.md) + +Permitted Execution Client Specifications URLs must anchor to a specific commit, and so must match this regular expression: + +```regex +^(https://github.com/ethereum/execution-specs/(blob|commit)/[0-9a-f]{40}/.*|https://github.com/ethereum/execution-specs/tree/[0-9a-f]{40}/.*)$ +``` + +### Execution Specification Tests + +Links to the Ethereum Execution Specification Tests (EEST) may be included using normal markdown syntax, such as: + +```markdown +[Ethereum Execution Specification Tests](https://github.com/ethereum/execution-spec-tests/blob/c9b9307ff320c9bb0ecb9a951aeab0da4d9d1684/README.md) +``` + +Which renders to: + +[Ethereum Execution Specification Tests](https://github.com/ethereum/execution-spec-tests/blob/c9b9307ff320c9bb0ecb9a951aeab0da4d9d1684/README.md) + +Permitted Execution Specification Tests URLs must anchor to a specific commit, and so must match one of these regular expressions: + +```regex +^https://(www\.)?github\.com/ethereum/execution-spec-tests/(blob|tree)/[a-f0-9]{40}/.+$ +``` + +```regex +^https://(www\.)?github\.com/ethereum/execution-spec-tests/commit/[a-f0-9]{40}$ +``` + +### Consensus Layer Specifications + +Links to specific commits of files within the Ethereum Consensus Layer Specifications may be included using normal markdown syntax, such as: + +```markdown +[Beacon Chain](https://github.com/ethereum/consensus-specs/blob/26695a9fdb747ecbe4f0bb9812fedbc402e5e18c/specs/sharding/beacon-chain.md) +``` + +Which renders to: + +[Beacon Chain](https://github.com/ethereum/consensus-specs/blob/26695a9fdb747ecbe4f0bb9812fedbc402e5e18c/specs/sharding/beacon-chain.md) + +Permitted Consensus Layer Specifications URLs must anchor to a specific commit, and so must match this regular expression: + +```regex +^https://github.com/ethereum/consensus-specs/(blob|commit)/[0-9a-f]{40}/.*$ +``` + +### Networking Specifications + +Links to specific commits of files within the Ethereum Networking Specifications may be included using normal markdown syntax, such as: + +```markdown +[Ethereum Wire Protocol](https://github.com/ethereum/devp2p/blob/40ab248bf7e017e83cc9812a4e048446709623e8/caps/eth.md) +``` + +Which renders as: + +[Ethereum Wire Protocol](https://github.com/ethereum/devp2p/blob/40ab248bf7e017e83cc9812a4e048446709623e8/caps/eth.md) + +Permitted Networking Specifications URLs must anchor to a specific commit, and so must match this regular expression: + +```regex +^https://github.com/ethereum/devp2p/(blob|commit)/[0-9a-f]{40}/.*$ +``` + +### Portal Specifications + +Links to specific commits of files within the Ethereum Portal Specifications may be included using normal markdown syntax, such as: + +```markdown +[Portal Wire Protocol](https://github.com/ethereum/portal-network-specs/blob/5e321567b67bded7527355be714993c24371de1a/portal-wire-protocol.md) +``` + +Which renders as: + +[Portal Wire Protocol](https://github.com/ethereum/portal-network-specs/blob/5e321567b67bded7527355be714993c24371de1a/portal-wire-protocol.md) + +Permitted Networking Specifications URLs must anchor to a specific commit, and so must match this regular expression: + +```regex +^https://github.com/ethereum/portal-network-specs/(blob|commit)/[0-9a-f]{40}/.*$ +``` + +### World Wide Web Consortium (W3C) + +Links to a W3C "Recommendation" status specification may be included using normal markdown syntax. For example, the following link would be allowed: + +```markdown +[Secure Contexts](https://www.w3.org/TR/2021/CRD-secure-contexts-20210918/) +``` + +Which renders as: + +[Secure Contexts](https://www.w3.org/TR/2021/CRD-secure-contexts-20210918/) + +Permitted W3C recommendation URLs MUST anchor to a specification in the technical reports namespace with a date, and so MUST match this regular expression: + +```regex +^https://www\.w3\.org/TR/[0-9][0-9][0-9][0-9]/.*$ +``` + +### Web Hypertext Application Technology Working Group (WHATWG) + +Links to WHATWG specifications may be included using normal markdown syntax, such as: + +```markdown +[HTML](https://html.spec.whatwg.org/commit-snapshots/578def68a9735a1e36610a6789245ddfc13d24e0/) +``` + +Which renders as: + +[HTML](https://html.spec.whatwg.org/commit-snapshots/578def68a9735a1e36610a6789245ddfc13d24e0/) + +Permitted WHATWG specification URLs must anchor to a specification defined in the `spec` subdomain (idea specifications are not allowed) and to a commit snapshot, and so must match this regular expression: + +```regex +^https:\/\/[a-z]*\.spec\.whatwg\.org/commit-snapshots/[0-9a-f]{40}/$ +``` + +Although not recommended by WHATWG, EIPs must anchor to a particular commit so that future readers can refer to the exact version of the living standard that existed at the time the EIP was finalized. This gives readers sufficient information to maintain compatibility, if they so choose, with the version referenced by the EIP and the current living standard. + +### Internet Engineering Task Force (IETF) + +Links to an IETF Request For Comment (RFC) specification may be included using normal markdown syntax, such as: + +```markdown +[RFC 8446](https://www.rfc-editor.org/rfc/rfc8446) +``` + +Which renders as: + +[RFC 8446](https://www.rfc-editor.org/rfc/rfc8446) + +Permitted IETF specification URLs MUST anchor to a specification with an assigned RFC number (meaning cannot reference internet drafts), and so MUST match this regular expression: + +```regex +^https:\/\/www.rfc-editor.org\/rfc\/.*$ +``` + +### Bitcoin Improvement Proposal + +Links to Bitcoin Improvement Proposals may be included using normal markdown syntax, such as: + +```markdown +[BIP 38](https://github.com/bitcoin/bips/blob/3db736243cd01389a4dfd98738204df1856dc5b9/bip-0038.mediawiki) +``` + +Which renders to: + +[BIP 38](https://github.com/bitcoin/bips/blob/3db736243cd01389a4dfd98738204df1856dc5b9/bip-0038.mediawiki) + +Permitted Bitcoin Improvement Proposal URLs must anchor to a specific commit, and so must match this regular expression: + +```regex +^(https://github.com/bitcoin/bips/blob/[0-9a-f]{40}/bip-[0-9]+\.mediawiki)$ +``` + +### National Vulnerability Database (NVD) + +Links to the Common Vulnerabilities and Exposures (CVE) system as published by the National Institute of Standards and Technology (NIST) may be included, provided they are qualified by the date of the most recent change, using the following syntax: + +```markdown +[CVE-2023-29638 (2023-10-17T10:14:15)](https://nvd.nist.gov/vuln/detail/CVE-2023-29638) +``` + +Which renders to: + +[CVE-2023-29638 (2023-10-17T10:14:15)](https://nvd.nist.gov/vuln/detail/CVE-2023-29638) + +### Chain Agnostic Improvement Proposals (CAIPs) + +Links to a Chain Agnostic Improvement Proposals (CAIPs) specification may be included using normal markdown syntax, such as: + +```markdown +[CAIP 10](https://github.com/ChainAgnostic/CAIPs/blob/5dd3a2f541d399a82bb32590b52ca4340b09f08b/CAIPs/caip-10.md) +``` + +Which renders to: + +[CAIP 10](https://github.com/ChainAgnostic/CAIPs/blob/5dd3a2f541d399a82bb32590b52ca4340b09f08b/CAIPs/caip-10.md) + +Permitted Chain Agnostic URLs must anchor to a specific commit, and so must match this regular expression: + +```regex +^(https://github.com/ChainAgnostic/CAIPs/blob/[0-9a-f]{40}/CAIPs/caip-[0-9]+\.md)$ +``` + +### Ethereum Yellow Paper + +Links to the Ethereum Yellow Paper may be included using normal markdown syntax, such as: + +```markdown +[Ethereum Yellow Paper](https://github.com/ethereum/yellowpaper/blob/9c601d6a58c44928d4f2b837c0350cec9d9259ed/paper.pdf) +``` + +Which renders to: + +[Ethereum Yellow Paper](https://github.com/ethereum/yellowpaper/blob/9c601d6a58c44928d4f2b837c0350cec9d9259ed/paper.pdf) + +Permitted Yellow Paper URLs must anchor to a specific commit, and so must match this regular expression: + +```regex +^(https://github\.com/ethereum/yellowpaper/blob/[0-9a-f]{40}/paper\.pdf)$ +``` + +### Execution Client Specification Tests + +Links to the Ethereum Execution Client Specification Tests may be included using normal markdown syntax, such as: + +```markdown +[Ethereum Execution Client Specification Tests](https://github.com/ethereum/execution-spec-tests/blob/d5a3188f122912e137aa2e21ed2a1403e806e424/README.md) +``` + +Which renders to: + +[Ethereum Execution Client Specification Tests](https://github.com/ethereum/execution-spec-tests/blob/d5a3188f122912e137aa2e21ed2a1403e806e424/README.md) + +Permitted Execution Client Specification Tests URLs must anchor to a specific commit, and so must match this regular expression: + +```regex +^(https://github.com/ethereum/execution-spec-tests/(blob|commit)/[0-9a-f]{40}/.*|https://github.com/ethereum/execution-spec-tests/tree/[0-9a-f]{40}/.*)$ +``` + +### Digital Object Identifier System + +Links qualified with a Digital Object Identifier (DOI) may be included using the following syntax: + +````markdown +This is a sentence with a footnote.[^1] + +[^1]: + ```csl-json + { + "type": "article", + "id": 1, + "author": [ + { + "family": "Jameson", + "given": "Hudson" + } + ], + "DOI": "00.0000/a00000-000-0000-y", + "title": "An Interesting Article", + "original-date": { + "date-parts": [ + [2022, 12, 31] + ] + }, + "URL": "https://sly-hub.invalid/00.0000/a00000-000-0000-y", + "custom": { + "additional-urls": [ + "https://example.com/an-interesting-article.pdf" + ] + } + } + ``` +```` + +Which renders to: + + + + +This is a sentence with a footnote.[^1] + +[^1]: + ```csl-json + { + "type": "article", + "id": 1, + "author": [ + { + "family": "Jameson", + "given": "Hudson" + } + ], + "DOI": "00.0000/a00000-000-0000-y", + "title": "An Interesting Article", + "original-date": { + "date-parts": [ + [2022, 12, 31] + ] + }, + "URL": "https://sly-hub.invalid/00.0000/a00000-000-0000-y", + "custom": { + "additional-urls": [ + "https://example.com/an-interesting-article.pdf" + ] + } + } + ``` + + + +See the [Citation Style Language Schema](https://resource.citationstyles.org/schema/v1.0/input/json/csl-data.json) for the supported fields. In addition to passing validation against that schema, references must include a DOI and at least one URL. + +The top-level URL field must resolve to a copy of the referenced document which can be viewed at zero cost. Values under `additional-urls` must also resolve to a copy of the referenced document, but may charge a fee. + +### Execution API Specification + +Links to the Ethereum Execution API Specification may be included using normal markdown syntax, such as: + +```markdown +[Ethereum Execution API Specification](https://github.com/ethereum/execution-apis/blob/dd00287101e368752ba264950585dde4b61cdc17/README.md) +``` + +Which renders to: + +[Ethereum Execution API Specification](https://github.com/ethereum/execution-apis/blob/dd00287101e368752ba264950585dde4b61cdc17/README.md) + +Permitted Execution API Specification URLs must anchor to a specific commit, and so must match this regular expression: + +```regex +^(https://github.com/ethereum/execution-apis/(blob|commit)/[0-9a-f]{40}/.*|https://github.com/ethereum/execution-apis/tree/[0-9a-f]{40}/.*)$ +``` + +## Linking to other EIPs + +References to other EIPs should follow the format `EIP-N` where `N` is the EIP number you are referring to. Each EIP that is referenced in an EIP **MUST** be accompanied by a relative markdown link the first time it is referenced, and **MAY** be accompanied by a link on subsequent references. The link **MUST** always be done via relative paths so that the links work in this GitHub repository, forks of this repository, the main EIPs site, mirrors of the main EIP site, etc. For example, you would link to this EIP as `./eip-1.md`. + +## Auxiliary Files + +Images, diagrams and auxiliary files should be included in a subdirectory of the `assets` folder for that EIP as follows: `assets/eip-N` (where **N** is to be replaced with the EIP number). When linking to an image in the EIP, use relative links such as `../assets/eip-1/image.png`. + +## Transferring EIP Ownership + +It occasionally becomes necessary to transfer ownership of EIPs to a new champion. In general, we'd like to retain the original author as a co-author of the transferred EIP, but that's really up to the original author. A good reason to transfer ownership is because the original author no longer has the time or interest in updating it or following through with the EIP process, or has fallen off the face of the 'net (i.e. is unreachable or isn't responding to email). A bad reason to transfer ownership is because you don't agree with the direction of the EIP. We try to build consensus around an EIP, but if that's not possible, you can always submit a competing EIP. + +If you are interested in assuming ownership of an EIP, send a message asking to take over, addressed to both the original author and the EIP editor. If the original author doesn't respond to the email in a timely manner, the EIP editor will make a unilateral decision (it's not like such decisions can't be reversed :)). + +## EIP Editors + +The current EIP editors are + +- Matt Garnett (@lightclient) +- Sam Wilson (@SamWilsn) +- Zainan Victor Zhou (@xinbenlv) +- Gajinder Singh (@g11tech) +- Jochem Brouwer (@jochem-brouwer) + +Emeritus EIP editors are + +- Alex Beregszaszi (@axic) +- Casey Detrio (@cdetrio) +- Gavin John (@Pandapip1) +- Greg Colvin (@gcolvin) +- Hudson Jameson (@Souptacular) +- Martin Becze (@wanderer) +- Micah Zoltu (@MicahZoltu) +- Nick Johnson (@arachnid) +- Nick Savers (@nicksavers) +- Vitalik Buterin (@vbuterin) + +If you would like to become an EIP editor, please check [EIP-5069](./eip-5069.md). + +## EIP Editor Responsibilities + +For each new EIP that comes in, an editor does the following: + +- Read the EIP to check if it is ready: sound and complete. The ideas must make technical sense, even if they don't seem likely to get to final status. +- The title should accurately describe the content. +- Check the EIP for language (spelling, grammar, sentence structure, etc.), markup (GitHub flavored Markdown), code style + +If the EIP isn't ready, the editor will send it back to the author for revision, with specific instructions. + +Once the EIP is ready for the repository, the EIP editor will: + +- Assign an EIP number (generally incremental; editors can reassign if number sniping is suspected) +- Merge the corresponding [pull request](https://github.com/ethereum/EIPs/pulls) +- Send a message back to the EIP author with the next step. + +Many EIPs are written and maintained by developers with write access to the Ethereum codebase. The EIP editors monitor EIP changes, and correct any structure, grammar, spelling, or markup mistakes we see. + +The editors don't pass judgment on EIPs. We merely do the administrative & editorial part. + +## Style Guide + +### Titles + +The `title` field in the preamble: + +- Should not include the word "standard" or any variation thereof; and +- Should not include the EIP's number. + +### Descriptions + +The `description` field in the preamble: + +- Should not include the word "standard" or any variation thereof; and +- Should not include the EIP's number. + +### EIP numbers + +When referring to an EIP with a `category` of `ERC`, it must be written in the hyphenated form `ERC-X` where `X` is that EIP's assigned number. When referring to EIPs with any other `category`, it must be written in the hyphenated form `EIP-X` where `X` is that EIP's assigned number. + +### RFC 2119 and RFC 8174 + +EIPs are encouraged to follow [RFC 2119](https://www.ietf.org/rfc/rfc2119.html) and [RFC 8174](https://www.ietf.org/rfc/rfc8174.html) for terminology and to insert the following at the beginning of the Specification section: + +> The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +## History + +This document was derived heavily from [Bitcoin's BIP-0001](https://github.com/bitcoin/bips) written by Amir Taaki which in turn was derived from [Python's PEP-0001](https://peps.python.org/). In many places text was simply copied and modified. Although the PEP-0001 text was written by Barry Warsaw, Jeremy Hylton, and David Goodger, they are not responsible for its use in the Ethereum Improvement Process, and should not be bothered with technical questions specific to Ethereum or the EIP. Please direct all comments to the EIP editors. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/ERCSpecification/erc-1404-restricted.md b/doc/ERCSpecification/erc-1404-restricted.md new file mode 100644 index 0000000..6e479d3 --- /dev/null +++ b/doc/ERCSpecification/erc-1404-restricted.md @@ -0,0 +1,81 @@ +| eip | title | authors | status | discussions-to | type | category | created | +| ---- | -------------------------------- | ------------------------------------------------------------ | ------ | ------------------------------------------------------------ | --------- | -------- | ---------- | +| 1404 | Simple Restricted Token Standard | Ron Gierlach <[@rongierlach](https://github.com/rongierlach)>, James Poole <[@pooleja](https://github.com/pooleja)>, Mason Borda <[@masonicGIT](https://github.com/masonicGIT)>, Lawson Baker <[@lwsnbaker](https://github.com/lwsnbaker)> | Draft | https://github.com/simple-restricted-token/simple-restricted-token/issues | Standards | ERC | 2018-07-27 | + +# Simple Restricted Token Standard + +## Simple Summary + +A simple and interoperable standard for issuing tokens with transfer restrictions. The following draws on input from top issuers, law firms, relevant US regulatory bodies, and exchanges. + +## Abstract + +Current ERC token standards have provided the community with a platform on which to develop a decentralized economy that is focused on building Ethereum applications for the real world. As these applications mature and face consumer adoption, they begin to interface with corporate governance requirements as well as regulations. They must not only be able to meet corporate and regulatory requirements but must also be able to integrate with technology platforms underpinning their associated businesses. What follows is a simple and extendable standard that seeks to ease the burden of integration for wallets, exchanges, and issuers. + +## Motivation + +Token issuers need a way to restrict transfers of ERC-20 tokens to be compliant with securities laws and other contractual obligations. Current implementations do not address these requirements. + +A few emergent examples: + +- Enforcing Token Lock-Up Periods +- Enforcing Passed AML/KYC Checks +- Private Real-Estate Investment Trusts +- Delaware General Corporations Law Shares + +Furthermore, standards adoption amongst token issuers has the potential to evolve into a dynamic and interoperable landscape of automated compliance. + +The following design gives greater freedom / upgradability to token issuers and simultaneously decreases the burden of integration for developers and exchanges. + +Additionally, we see fit to provide a pattern by which human-readable messages may be returned when token transfers are reverted. Transparency as to *why* a token's transfer was reverted is of equal importance to the successful enforcement of the transfer restriction itself. + +A widely adopted standard for detecting restrictions and messaging errors within token transfers will highly convenience the exchanges, wallets, and issuers of the future. + +## Specification + +The ERC-20 token provides the following basic features: + +``` +contract ERC20 { + function totalSupply() public view returns (uint256); + function balanceOf(address who) public view returns (uint256); + function transfer(address to, uint256 value) public returns (bool); + function allowance(address owner, address spender) public view returns (uint256); + function transferFrom(address from, address to, uint256 value) public returns (bool); + function approve(address spender, uint256 value) public returns (bool); + event Approval(address indexed owner, address indexed spender, uint256 value); + event Transfer(address indexed from, address indexed to, uint256 value); +} +``` + + + +The ERC-1404 standard builds on ERC-20's interface, adding two functions: + +``` +contract ERC1404 is ERC20 { + function detectTransferRestriction (address from, address to, uint256 value) public view returns (uint8); + function messageForTransferRestriction (uint8 restrictionCode) public view returns (string); +} +``` + + + +The logic of `detectTransferRestriction` and `messageForTransferRestriction` are left up to the issuer. + +The only requirement is that `detectTransferRestriction` must be evaluated inside a token's `transfer` and `transferFrom` methods. + +If, inside these transfer methods, `detectTransferRestriction` returns a value other than `0`, the transaction should be reverted. + +## Rationale + +The standard proposes two functions on top of the ERC-20 standard. Let's discuss the rationale for each. + +1. `detectTransferRestriction` - This function is where an issuer enforces the restriction logic of their token transfers. Some examples of this might include, checking if the token recipient is whitelisted, checking if a sender's tokens are frozen in a lock-up period, etc. Because implementation is up to the issuer, this function serves solely to standardize *where* execution of such logic should be initiated. Additionally, 3rd parties may publicly call this function to check the expected outcome of a transfer. Because this function returns a `uint8` code rather than a boolean or just reverting, it allows the function caller to know the reason why a transfer might fail and report this to relevant counterparties. +2. `messageForTransferRestriction` - This function is effectively an accessor for the "message", a human-readable explanation as to *why* a transaction is restricted. By standardizing message look-ups, we empower user interface builders to effectively report errors to users. + +## Backwards Compatibility + +By design ERC-1404 is fully backwards compatible with ERC-20. +Some examples of how it may be integrated with common types of restricted tokens may be found [here](https://github.com/simple-restricted-token/simple-restricted-token-standard#readme). + diff --git a/doc/ERCSpecification/erc-2612.md b/doc/ERCSpecification/erc-2612.md new file mode 100644 index 0000000..a70cf59 --- /dev/null +++ b/doc/ERCSpecification/erc-2612.md @@ -0,0 +1,198 @@ +--- +eip: 2612 +title: Permit Extension for EIP-20 Signed Approvals +description: EIP-20 approvals via EIP-712 secp256k1 signatures +author: Martin Lundfall (@Mrchico) +discussions-to: https://github.com/ethereum/EIPs/issues/2613 +status: Final +type: Standards Track +category: ERC +created: 2020-04-13 +requires: 20, 712 +--- + +## Abstract + +Arguably one of the main reasons for the success of [EIP-20](./eip-20.md) tokens lies in the interplay between `approve` and `transferFrom`, which allows for tokens to not only be transferred between externally owned accounts (EOA), but to be used in other contracts under application specific conditions by abstracting away `msg.sender` as the defining mechanism for token access control. + +However, a limiting factor in this design stems from the fact that the EIP-20 `approve` function itself is defined in terms of `msg.sender`. This means that user's _initial action_ involving EIP-20 tokens must be performed by an EOA (_but see Note below_). If the user needs to interact with a smart contract, then they need to make 2 transactions (`approve` and the smart contract call which will internally call `transferFrom`). Even in the simple use case of paying another person, they need to hold ETH to pay for transaction gas costs. + +This ERC extends the EIP-20 standard with a new function `permit`, which allows users to modify the `allowance` mapping using a signed message, instead of through `msg.sender`. + +For an improved user experience, the signed data is structured following [EIP-712](./eip-712.md), which already has wide spread adoption in major RPC providers. + +**_Note:_** EIP-20 must be performed by an EOA unless the address owning the token is actually a contract wallet. Although contract wallets solves many of the same problems that motivates this EIP, they are currently only scarcely adopted in the ecosystem. Contract wallets suffer from a UX problem -- since they separate the EOA `owner` of the contract wallet from the contract wallet itself (which is meant to carry out actions on the `owner`s behalf and holds all of their funds), user interfaces need to be specifically designed to support them. The `permit` pattern reaps many of the same benefits while requiring little to no change in user interfaces. + +## Motivation + +While EIP-20 tokens have become ubiquitous in the Ethereum ecosystem, their status remains that of second class tokens from the perspective of the protocol. The ability for users to interact with Ethereum without holding any ETH has been a long outstanding goal and the subject of many EIPs. + +So far, many of these proposals have seen very little adoption, and the ones that have been adopted (such as [EIP-777](./eip-777.md)), introduce a lot of additional functionality, causing unexpected behavior in mainstream contracts. + +This ERC proposes an alternative solution which is designed to be as minimal as possible and to only address _one problem_: the lack of abstraction in the EIP-20 `approve` method. + +While it may be tempting to introduce `*_by_signature` counterparts for every EIP-20 function, they are intentionally left out of this EIP-20 for two reasons: + +- the desired specifics of such functions, such as decision regarding fees for `transfer_by_signature`, possible batching algorithms, varies depending on the use case, and, +- they can be implemented using a combination of `permit` and additional helper contracts without loss of generality. + +## Specification + +Compliant contracts must implement 3 new functions in addition to EIP-20: + +```sol +function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external +function nonces(address owner) external view returns (uint) +function DOMAIN_SEPARATOR() external view returns (bytes32) +``` + +The semantics of which are as follows: + +For all addresses `owner`, `spender`, uint256s `value`, `deadline` and `nonce`, uint8 `v`, bytes32 `r` and `s`, +a call to `permit(owner, spender, value, deadline, v, r, s)` will set +`allowance[owner][spender]` to `value`, +increment `nonces[owner]` by 1, +and emit a corresponding `Approval` event, +if and only if the following conditions are met: + +- The current blocktime is less than or equal to `deadline`. +- `owner` is not the zero address. +- `nonces[owner]` (before the state update) is equal to `nonce`. +- `r`, `s` and `v` is a valid `secp256k1` signature from `owner` of the message: + +If any of these conditions are not met, the `permit` call must revert. + +```sol +keccak256(abi.encodePacked( + hex"1901", + DOMAIN_SEPARATOR, + keccak256(abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + spender, + value, + nonce, + deadline)) +)) +``` + +where `DOMAIN_SEPARATOR` is defined according to EIP-712. The `DOMAIN_SEPARATOR` should be unique to the contract and chain to prevent replay attacks from other domains, +and satisfy the requirements of EIP-712, but is otherwise unconstrained. +A common choice for `DOMAIN_SEPARATOR` is: + +```solidity +DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), + keccak256(bytes(name)), + keccak256(bytes(version)), + chainid, + address(this) +)); +``` + +In other words, the message is the EIP-712 typed structure: + +```js +{ + "types": { + "EIP712Domain": [ + { + "name": "name", + "type": "string" + }, + { + "name": "version", + "type": "string" + }, + { + "name": "chainId", + "type": "uint256" + }, + { + "name": "verifyingContract", + "type": "address" + } + ], + "Permit": [ + { + "name": "owner", + "type": "address" + }, + { + "name": "spender", + "type": "address" + }, + { + "name": "value", + "type": "uint256" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "deadline", + "type": "uint256" + } + ], + }, + "primaryType": "Permit", + "domain": { + "name": erc20name, + "version": version, + "chainId": chainid, + "verifyingContract": tokenAddress + }, + "message": { + "owner": owner, + "spender": spender, + "value": value, + "nonce": nonce, + "deadline": deadline + } +} +``` + +Note that nowhere in this definition we refer to `msg.sender`. The caller of the `permit` function can be any address. + +## Rationale + +The `permit` function is sufficient for enabling any operation involving EIP-20 tokens to be paid for using the token itself, rather than using ETH. + +The `nonces` mapping is given for replay protection. + +A common use case of `permit` has a relayer submit a `Permit` on behalf of the `owner`. In this scenario, the relaying party is essentially given a free option to submit or withhold the `Permit`. If this is a cause of concern, the `owner` can limit the time a `Permit` is valid for by setting `deadline` to a value in the near future. The `deadline` argument can be set to `uint(-1)` to create `Permit`s that effectively never expire. + +EIP-712 typed messages are included because of its wide spread adoption in many wallet providers. + +## Backwards Compatibility + +There are already a couple of `permit` functions in token contracts implemented in contracts in the wild, most notably the one introduced in the `dai.sol`. + +Its implementation differs slightly from the presentation here in that: + +- instead of taking a `value` argument, it takes a bool `allowed`, setting approval to 0 or `uint(-1)`. +- the `deadline` argument is instead called `expiry`. This is not just a syntactic change, as it effects the contents of the signed message. + +There is also an implementation in the token `Stake` (Ethereum address `0x0Ae055097C6d159879521C384F1D2123D1f195e6`) with the same ABI as `dai` but with different semantics: it lets users issue "expiring approvals", that only allow `transferFrom` to occur while `expiry >= block.timestamp`. + +The specification presented here is in line with the implementation in Uniswap V2. + +The requirement to revert if the permit is invalid was added when the EIP was already widely deployed, but at the moment it was consistent with all found implementations. + +## Security Considerations + +Though the signer of a `Permit` may have a certain party in mind to submit their transaction, another party can always front run this transaction and call `permit` before the intended party. The end result is the same for the `Permit` signer, however. + +Since the ecrecover precompile fails silently and just returns the zero address as `signer` when given malformed messages, it is important to ensure `owner != address(0)` to avoid `permit` from creating an approval to spend "zombie funds" belong to the zero address. + +Signed `Permit` messages are censorable. The relaying party can always choose to not submit the `Permit` after having received it, withholding the option to submit it. The `deadline` parameter is one mitigation to this. If the signing party holds ETH they can also just submit the `Permit` themselves, which can render previously signed `Permit`s invalid. + +The standard EIP-20 race condition for approvals (SWC-114) applies to `permit` as well. + +If the `DOMAIN_SEPARATOR` contains the `chainId` and is defined at contract deployment instead of reconstructed for every signature, there is a risk of possible replay attacks between chains in the event of a future chain split. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/ERCSpecification/erc-2980.md b/doc/ERCSpecification/erc-2980.md new file mode 100644 index 0000000..ec74e0e --- /dev/null +++ b/doc/ERCSpecification/erc-2980.md @@ -0,0 +1,197 @@ +--- +eip: 2980 +title: Swiss Compliant Asset Token +description: An interface for asset tokens, compliant with Swiss Law and compatible with [ERC-20](./eip-20.md). +author: Gianluca Perletti (@Perlets9), Alan Scarpellini (@alanscarpellini), Roberto Gorini (@robertogorini), Manuel Olivi (@manvel79) +discussions-to: https://github.com/ethereum/EIPs/issues/2983 +status: Stagnant +type: Standards Track +category: ERC +created: 2020-09-08 +requires: 20 +--- + +## Abstract + +This new standard is an [ERC-20](./eip-20.md) compatible token with restrictions that comply with the following Swiss laws: the [Stock Exchange Act](../assets/eip-2980/Swiss-Confederation-SESTA.pdf), the [Banking Act](../assets/eip-2980/Swiss-Confederation-BA.pdf), the [Financial Market Infrastructure Act](../assets/eip-2980/Swiss-Confederation-FMIA.pdf), the [Act on Collective Investment Schemes](../assets/eip-2980/Swiss-Confederation-CISA.pdf) and the [Anti-Money Laundering Act](../assets/eip-2980/Swiss-Confederation-AMLA.pdf). The [Financial Services Act](../assets/eip-2980/Swiss-Confederation-FINSA.pdf) and the [Financial Institutions Act](../assets/eip-2980/Swiss-Confederation-FINIA.pdf) must also be considered. The solution achieved meet also the European jurisdiction. + +This new standard meets the new era of asset tokens (known also as "security tokens"). These new methods manage securities ownership during issuance and trading. The issuer is the only role that can manage a white-listing and the only one that is allowed to execute “freeze” or “revoke” functions. + +## Motivation + +In its ICO guidance dated February 16, 2018, FINMA (Swiss Financial Market Supervisory Authority) defines asset tokens as tokens representing assets and/or relative rights ([FINMA ICO Guidelines](../assets/eip-2980/Finma-ICO-Guidelines.pdf)). It explicitly mentions that asset tokens are analogous to and can economically represent shares, bonds, or derivatives. The long list of relevant financial market laws mentioned above reveal that we need more methods than with Payment and Utility Token. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. + +The words "asset tokens" and "security tokens" can be considered synonymous. + +Every ERC-2980 compliant contract MUST implement the ERC-2980 interface. + +### ERC-2980 (Token Contract) + +``` solidity +interface ERC2980 extends ERC20 { + + /// @dev This emits when funds are reassigned + event FundsReassigned(address from, address to, uint256 amount); + + /// @dev This emits when funds are revoked + event FundsRevoked(address from, uint256 amount); + + /// @dev This emits when an address is frozen + event FundsFrozen(address target); + + /** + * @dev getter to determine if address is in frozenlist + */ + function frozenlist(address _operator) external view returns (bool); + + /** + * @dev getter to determine if address is in whitelist + */ + function whitelist(address _operator) external view returns (bool); + +} +``` + +The ERC-2980 extends [ERC-20](./eip-20.md). Due to the indivisible nature of asset tokens, the decimals number MUST be zero. + +### Whitelist and Frozenlist + +The accomplishment of the Swiss Law requirements is achieved by the use of two distinct lists of address: the Whitelist and the Frozenlist. +Addresses can be added to one or the other list at any time by operators with special privileges, called Issuers, and described below. +Although these lists may look similar, they differ for the following reasons: the Whitelist members are the only ones who can receive tokens from other addresses. There is no restriction on the possibility that these addresses can transfer the tokens already in their ownership. +This can occur when an address, present in the Whitelist, is removed from this list, without however being put in the Frozenlist and remaining in possession of its tokens. +On the other hand, the addresses assigned to the Frozenlist, as suggested by the name itself, have to be considered "frozen", so they cannot either receive tokens or send tokens to anyone. + +Below is an example interface for the implementation of a whitelist-compatible and a frozenlist-compratible contract. + +``` solidity +Interface Whitelistable { + + /** + * @dev add an address to the whitelist + * Throws unless `msg.sender` is an Issuer operator + * @param _operator address to add + * @return true if the address was added to the whitelist, false if the address was already in the whitelist + */ + function addAddressToWhitelist(address _operator) external returns (bool); + + /** + * @dev remove an address from the whitelist + * Throws unless `msg.sender` is an Issuer operator + * @param _operator address to remove + * @return true if the address was removed from the whitelist, false if the address wasn't in the whitelist in the first place + */ + function removeAddressFromWhitelist(address _operator) external returns (bool); + +} + +Interface Freezable { + + /** + * @dev add an address to the frozenlist + * Throws unless `msg.sender` is an Issuer operator + * @param _operator address to add + * @return true if the address was added to the frozenlist, false if the address was already in the frozenlist + */ + function addAddressToFrozenlist(address _operator) external returns (bool); + + /** + * @dev remove an address from the frozenlist + * Throws unless `msg.sender` is an Issuer operator + * @param _operator address to remove + * @return true if the address was removed from the frozenlist, false if the address wasn't in the frozenlist in the first place + */ + function removeAddressFromFrozenlist(address _operator) external returns (bool); + +} +``` + +### Issuers + +A key role is played by the Issuer. This figure has the permission to manage Whitelists and Frozenlists, to revoke tokens and reassign them and to transfer the role to another address. No restrictions on the possibility to have more than one Issuer per contract. Issuers are nominated by the Owner of the contract, who also is in charge of remove the role. The possibility of nominating the Owner itself as Issuer at the time of contract creation (or immediately after) is not excluded. + +Below is an example interface for the implementation of the Issuer functionalities. + +``` solidity +Interface Issuable { + + /** + * @dev getter to determine if address has issuer role + */ + function isIssuer(address _addr) external view returns (bool); + + /** + * @dev add a new issuer address + * Throws unless `msg.sender` is the contract owner + * @param _operator address + * @return true if the address was not an issuer, false if the address was already an issuer + */ + function addIssuer(address _operator) external returns (bool); + + /** + * @dev remove an address from issuers + * Throws unless `msg.sender` is the contract owner + * @param _operator address + * @return true if the address has been removed from issuers, false if the address wasn't in the issuer list in the first place + */ + function removeIssuer(address _operator) external returns (bool); + + /** + * @dev Allows the current issuer to transfer its role to a newIssuer + * Throws unless `msg.sender` is an Issuer operator + * @param _newIssuer The address to transfer the issuer role to + */ + function transferIssuer(address _newIssuer) external; + +} +``` + +### Revoke and Reassign + +Revoke and Reassign methods allow Issuers to move tokens from addresses, even if they are in the Frozenlist. The Revoke method transfers the entire balance of the target address to the Issuer who invoked the method. The Reassign method transfers the entire balance of the target address to another address. These rights for these operations MUST be allowed only to Issuers. + +Below is an example interface for the implementation of the Revoke and Reassign functionalities. + +``` solidity +Interface RevokableAndReassignable { + + /** + * @dev Allows the current Issuer to transfer token from an address to itself + * Throws unless `msg.sender` is an Issuer operator + * @param _from The address from which the tokens are withdrawn + */ + function revoke(address _from) external; + + /** + * @dev Allows the current Issuer to transfer token from an address to another + * Throws unless `msg.sender` is an Issuer operator + * @param _from The address from which the tokens are withdrawn + * @param _to The address who receives the tokens + */ + function reassign(address _from, address _to) external; + +} +``` + +## Rationale + +There are currently no token standards that expressly facilitate conformity to securities law and related regulations. EIP-1404 (Simple Restricted Token Standard) it’s not enough to address FINMA requirements around re-issuing securities to Investors. +In Swiss law, an issuer must eventually enforce the restrictions of their token transfer with a “freeze” function. The token must be “revocable”, and we need to apply a white-list method for AML/KYC checks. + +## Backwards Compatibility + +This EIP does not introduce backward incompatibilities and is backward compatible with the older ERC-20 token standard. +This standard allows the implementation of ERC-20 functions transfer, transferFrom, approve and allowance alongside to make a token fully compatible with ERC-20. +The token MAY implement decimals() for backward compatibility with ERC-20. If implemented, it MUST always return 0. + +## Security Considerations + +The security considerations mainly concern the role played by the Issuers. This figure, in fact, is not generally present in common ERC-20 tokens but has very powerful rights that allow him to move tokens without being in possession and freeze other addresses, preventing them from transferring tokens. It must be the responsibility of the owner to ensure that the addresses that receive this charge remain in possession of it only for the time for which they have been designated to do so, thus preventing any abuse. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/ERCSpecification/erc-3643.md b/doc/ERCSpecification/erc-3643.md new file mode 100644 index 0000000..c4a5d3a --- /dev/null +++ b/doc/ERCSpecification/erc-3643.md @@ -0,0 +1,414 @@ +--- +eip: 3643 +title: T-REX - Token for Regulated EXchanges +description: An institutional grade security token contract that provides interfaces for the management and compliant transfer of security tokens. +author: Joachim Lebrun (@Joachim-Lebrun), Tony Malghem (@TonyMalghem), Kevin Thizy (@Nakasar), Luc Falempin (@lfalempin), Adam Boudjemaa (@Aboudjem) +discussions-to: https://ethereum-magicians.org/t/eip-3643-proposition-of-the-t-rex-token-standard-for-securities/6844 +status: Final +type: Standards Track +category: ERC +created: 2021-07-09 +requires: 20, 173 +--- + +## Abstract + +The T-REX token is an institutional grade security token standard. This standard provides a library of interfaces for the management and compliant transfer of security tokens, using an automated onchain validator system leveraging onchain identities for eligibility checks. + +The standard defines several interfaces that are described hereunder: + +- Token +- Identity Registry +- Identity Registry Storage +- Compliance +- Trusted Issuers Registry +- Claim Topics Registry + +## Motivation + +The advent of blockchain technology has brought about a new era of efficiency, accessibility, and liquidity in the world of asset transfer. This is particularly evident in the realm of cryptocurrencies, where users can transfer token ownership peer-to-peer without intermediaries. However, when it comes to tokenized securities or security tokens, the situation is more complex due to the need for compliance with securities laws. These tokens cannot be permissionless like utility tokens; they must be permissioned to track ownership and ensure that only eligible investors can hold tokens. + +The existing Ethereum protocol, while powerful and versatile, does not fully address the unique challenges posed by security tokens. There is a need for a standard that supports compliant issuance and management of permissioned tokens, suitable for representing a wide range of asset classes, including small businesses and real estate. + +The proposed [ERC-3643](./eip-3643.md) standard is motivated by this need. It aims to provide a comprehensive framework for managing the lifecycle of security tokens, from issuance to transfers between eligible investors, while enforcing compliance rules at every stage. The standard also supports additional features such as token pausing and freezing, which can be used to manage the token in response to regulatory requirements or changes in the status of the token or its holders. + +Moreover, the standard is designed to work in conjunction with an on-chain Identity system, allowing for the validation of the identities and credentials of investors through signed attestations issued by trusted claim issuers. This ensures compliance with legal and regulatory requirements for the trading of security tokens. + +In summary, the motivation behind the proposed standard is to bring the benefits of blockchain technology to the world of securities, while ensuring compliance with existing securities laws. It aims to provide a robust, flexible, and efficient framework for the issuance and management of security tokens, thereby accelerating the evolution of capital markets. + +## Specification + +The proposed standard has the following requirements: + +- **MUST** be [ERC-20](./eip-20.md) compatible. +- **MUST** be used in combination with an onchain Identity system +- **MUST** be able to apply any rule of compliance that is required by the regulator or by the token issuer (about the factors of eligibility of an identity or about the rules of the token itself) +- **MUST** have a standard interface to pre-check if a transfer is going to pass or fail before sending it to the blockchain +- **MUST** have a recovery system in case an investor loses access to his private key +- **MUST** be able to freeze tokens on the wallet of investors if needed, partially or totally +- **MUST** have the possibility to pause the token +- **MUST** be able to mint and burn tokens +- **MUST** define an Agent role and an Owner (token issuer) role +- **MUST** be able to force transfers from an Agent wallet +- **MUST** be able to issue transactions in batch (to save gas and to have all the transactions performed in the same block) + +While this standard is backwards compatible with ERC-20 and all ERC-20 functions can be called on an ERC-3643 token, the implementation of these functions differs due to the permissioned nature of ERC-3643. Each token transfer under this standard involves a compliance check to validate the transfer and the eligibility of the stakeholder’s identities. + +### Agent Role Interface + +The standard defines an Agent role, which is crucial for managing access to various functions of the smart contracts. The interface for the Agent role is as follows: + +```solidity +interface IAgentRole { + + // events + event AgentAdded(address indexed _agent); + event AgentRemoved(address indexed _agent); + + // functions + // setters + function addAgent(address _agent) external; + function removeAgent(address _agent) external; + + // getters + function isAgent(address _agent) external view returns (bool); +} + ``` + +The `IAgentRole` interface allows for the addition and removal of agents, as well as checking if an address is an agent. In this standard, it is the owner role, as defined by [ERC-173](./eip-173.md), that has the responsibility of appointing and removing agents. Any contract that fulfills the role of a Token contract or an Identity Registry within the context of this standard must be compatible with the `IAgentRole` interface. + +### Main functions + +#### Transfer + +To be able to perform a transfer on T-REX you need to fulfill several conditions : + +- The sender **MUST** hold enough free balance (total balance - frozen tokens, if any) +- The receiver **MUST** be whitelisted on the Identity Registry and verified (hold the necessary claims on his onchain Identity) +- The sender's wallet **MUST NOT** be frozen +- The receiver's wallet **MUST NOT** be frozen +- The token **MUST NOT** be paused +- The transfer **MUST** respect all the rules of compliance defined in the Compliance smart contract (canTransfer needs to return TRUE) + +Here is an example of `transfer` function implementation : + +```solidity +function transfer(address _to, uint256 _amount) public override whenNotPaused returns (bool) { + require(!_frozen[_to] && !_frozen[msg.sender], "ERC-3643: Frozen wallet"); + require(_amount <= balanceOf(msg.sender) - (_frozenTokens[msg.sender]), "ERC-3643: Insufficient Balance"); + require( _tokenIdentityRegistry.isVerified(to), "ERC-3643: Invalid identity" ); + require( _tokenCompliance.canTransfer(from, to, amount), "ERC-3643: Compliance failure" ); + _transfer(msg.sender, _to, _amount); + _tokenCompliance.transferred(msg.sender, _to, _amount); + return true; + } + ``` + +The `transferFrom` function works the same way while the `mint` function and the `forcedTransfer` function only require the receiver to be whitelisted and verified on the Identity Registry (they bypass the compliance rules). The `burn` function bypasses all checks on eligibility. + +#### isVerified + +The `isVerified` function is called from within the transfer functions `transfer`, `transferFrom`, `mint` and +`forcedTransfer` to instruct the `Identity Registry` to check if the receiver is a valid investor, i.e. if his +wallet address is in the `Identity Registry` of the token, and if the `Identity`contract linked to his wallet +contains the claims (see [Claim Holder](../assets/eip-3643/ONCHAINID/IERC735.sol)) required in the `Claim Topics Registry` and +if these claims are signed by an authorized Claim Issuer as required in the `Trusted Issuers Registry`. +If all the requirements are fulfilled, the `isVerified` function returns `TRUE`, otherwise it returns `FALSE`. An +implementation of this function can be found on the T-REX repository of Tokeny. + +#### canTransfer + +The `canTransfer` function is also called from within transfer functions. This function checks if the transfer is compliant with global compliance rules applied to the token, in opposition with `isVerified` that only checks the eligibility of an investor to hold and receive tokens, the `canTransfer` function is looking at global compliance rules, e.g. check if the transfer is compliant in the case there is a fixed maximum number of token holders to respect (can be a limited number of holders per country as well), check if the transfer respects rules setting a maximum amount of tokens per investor, ... +If all the requirements are fulfilled, the `canTransfer` function will return `TRUE` otherwise it will return +`FALSE` and the transfer will not be allowed to happen. An implementation of this function can be found on the T-REX +repository of Tokeny. + +#### Other functions + +Description of other functions of the ERC-3643 can be found in the `interfaces` folder. An implementation of the +ERC-3643 suite of smart contracts can be found on the T-REX repository of Tokeny. + +### Token interface + +ERC-3643 permissioned tokens build upon the standard ERC-20 structure, but with additional functions to ensure compliance in the transactions of the security tokens. The functions `transfer` and `transferFrom` are implemented in a conditional way, allowing them to proceed with a transfer only if the transaction is valid. The permissioned tokens are allowed to be transferred only to validated counterparties, in order to avoid tokens being held in wallets/Identity contracts of ineligible/unauthorized investors. The ERC-3643 standard also supports the recovery of security tokens in case an investor loses access to their wallet private key. A history of recovered tokens is maintained on the blockchain for transparency reasons. + +ERC-3643 tokens implement a range of additional functions to enable the owner or their appointed agents to manage supply, transfer rules, lockups, and any other requirements in the management of a security. The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of appointing agents. Any contract that fulfills the role of a Token contract within the context of this standard must be compatible with the `IAgentRole` interface. + +A detailed description of the functions can be found in the [interfaces folder](../assets/eip-3643/interfaces/IERC3643.sol). + +```solidity +interface IERC3643 is IERC20 { + + // events + event UpdatedTokenInformation(string _newName, string _newSymbol, uint8 _newDecimals, string _newVersion, address _newOnchainID); + event IdentityRegistryAdded(address indexed _identityRegistry); + event ComplianceAdded(address indexed _compliance); + event RecoverySuccess(address _lostWallet, address _newWallet, address _investorOnchainID); + event AddressFrozen(address indexed _userAddress, bool indexed _isFrozen, address indexed _owner); + event TokensFrozen(address indexed _userAddress, uint256 _amount); + event TokensUnfrozen(address indexed _userAddress, uint256 _amount); + event Paused(address _userAddress); + event Unpaused(address _userAddress); + + + // functions + // getters + function onchainID() external view returns (address); + function version() external view returns (string memory); + function identityRegistry() external view returns (IIdentityRegistry); + function compliance() external view returns (ICompliance); + function paused() external view returns (bool); + function isFrozen(address _userAddress) external view returns (bool); + function getFrozenTokens(address _userAddress) external view returns (uint256); + + // setters + function setName(string calldata _name) external; + function setSymbol(string calldata _symbol) external; + function setOnchainID(address _onchainID) external; + function pause() external; + function unpause() external; + function setAddressFrozen(address _userAddress, bool _freeze) external; + function freezePartialTokens(address _userAddress, uint256 _amount) external; + function unfreezePartialTokens(address _userAddress, uint256 _amount) external; + function setIdentityRegistry(address _identityRegistry) external; + function setCompliance(address _compliance) external; + + // transfer actions + function forcedTransfer(address _from, address _to, uint256 _amount) external returns (bool); + function mint(address _to, uint256 _amount) external; + function burn(address _userAddress, uint256 _amount) external; + function recoveryAddress(address _lostWallet, address _newWallet, address _investorOnchainID) external returns (bool); + + // batch functions + function batchTransfer(address[] calldata _toList, uint256[] calldata _amounts) external; + function batchForcedTransfer(address[] calldata _fromList, address[] calldata _toList, uint256[] calldata _amounts) external; + function batchMint(address[] calldata _toList, uint256[] calldata _amounts) external; + function batchBurn(address[] calldata _userAddresses, uint256[] calldata _amounts) external; + function batchSetAddressFrozen(address[] calldata _userAddresses, bool[] calldata _freeze) external; + function batchFreezePartialTokens(address[] calldata _userAddresses, uint256[] calldata _amounts) external; + function batchUnfreezePartialTokens(address[] calldata _userAddresses, uint256[] calldata _amounts) external; +} + +``` + +### Identity Registry Interface + +The Identity Registry is linked to storage that contains a dynamic whitelist of identities. It establishes the link between a wallet address, an Identity smart contract, and a country code corresponding to the investor's country of residence. This country code is set in accordance with the ISO-3166 standard. The Identity Registry also includes a function called `isVerified()`, which returns a status based on the validity of claims (as per the security token requirements) in the user’s Identity contract. + +The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of appointing agents. Any contract that fulfills the role of an Identity Registry within the context of this standard must be compatible with the `IAgentRole` interface. The Identity Registry is managed by the agent wallet(s), meaning only the agent(s) can add or remove identities in the registry. Note that the agent role on the Identity Registry is set by the owner, therefore the owner could set themselves as the agent if they want to maintain full control. There is a specific identity registry for each security token. + +A detailed description of the functions can be found in the [interfaces folder](../assets/eip-3643/interfaces/IIdentityRegistry.sol). + +Note that [`IClaimIssuer`](../assets/eip-3643/ONCHAINID/IClaimIssuer.sol) and [`IIdentity`](../assets/eip-3643/ONCHAINID/IIdentity.sol) are needed in this interface as they are required for the Identity eligibility checks. + +```solidity +interface IIdentityRegistry { + + + // events + event ClaimTopicsRegistrySet(address indexed claimTopicsRegistry); + event IdentityStorageSet(address indexed identityStorage); + event TrustedIssuersRegistrySet(address indexed trustedIssuersRegistry); + event IdentityRegistered(address indexed investorAddress, IIdentity indexed identity); + event IdentityRemoved(address indexed investorAddress, IIdentity indexed identity); + event IdentityUpdated(IIdentity indexed oldIdentity, IIdentity indexed newIdentity); + event CountryUpdated(address indexed investorAddress, uint16 indexed country); + + + // functions + // identity registry getters + function identityStorage() external view returns (IIdentityRegistryStorage); + function issuersRegistry() external view returns (ITrustedIssuersRegistry); + function topicsRegistry() external view returns (IClaimTopicsRegistry); + + //identity registry setters + function setIdentityRegistryStorage(address _identityRegistryStorage) external; + function setClaimTopicsRegistry(address _claimTopicsRegistry) external; + function setTrustedIssuersRegistry(address _trustedIssuersRegistry) external; + + // registry actions + function registerIdentity(address _userAddress, IIdentity _identity, uint16 _country) external; + function deleteIdentity(address _userAddress) external; + function updateCountry(address _userAddress, uint16 _country) external; + function updateIdentity(address _userAddress, IIdentity _identity) external; + function batchRegisterIdentity(address[] calldata _userAddresses, IIdentity[] calldata _identities, uint16[] calldata _countries) external; + + // registry consultation + function contains(address _userAddress) external view returns (bool); + function isVerified(address _userAddress) external view returns (bool); + function identity(address _userAddress) external view returns (IIdentity); + function investorCountry(address _userAddress) external view returns (uint16); +} +``` + +### Identity Registry Storage Interface + +The Identity Registry Storage stores the identity addresses of all the authorized investors in the security token(s) linked to the storage contract. These are all identities of investors who have been authorized to hold the token(s) after having gone through the appropriate KYC and eligibility checks. The Identity Registry Storage can be bound to one or several Identity Registry contract(s). The goal of the Identity Registry storage is to separate the Identity Registry functions and specifications from its storage. This way, it is possible to keep one single Identity Registry contract per token, with its own Trusted Issuers Registry and Claim Topics Registry, but with a shared whitelist of investors used by the `isVerifed()` function implemented in the Identity Registries to check the eligibility of the receiver in a transfer transaction. + +The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of appointing agents(in this case through the `bindIdentityRegistry` function). Any contract that fulfills the role of an Identity Registry Storage within the context of this standard must be compatible with the `IAgentRole` interface. The Identity Registry Storage is managed by the agent addresses (i.e. the bound Identity Registries), meaning only the agent(s) can add or remove identities in the registry. Note that the agent role on the Identity Registry Storage is set by the owner, therefore the owner could set themselves as the agent if they want to modify the storage manually. Otherwise it is the bound Identity Registries that are using the agent role to write in the Identity Registry Storage. + +A detailed description of the functions can be found in the [interfaces folder](../assets/eip-3643/interfaces/IIdentityRegistryStorage.sol). + +```solidity +interface IIdentityRegistryStorage { + + //events + event IdentityStored(address indexed investorAddress, IIdentity indexed identity); + event IdentityUnstored(address indexed investorAddress, IIdentity indexed identity); + event IdentityModified(IIdentity indexed oldIdentity, IIdentity indexed newIdentity); + event CountryModified(address indexed investorAddress, uint16 indexed country); + event IdentityRegistryBound(address indexed identityRegistry); + event IdentityRegistryUnbound(address indexed identityRegistry); + + //functions + // storage related functions + function storedIdentity(address _userAddress) external view returns (IIdentity); + function storedInvestorCountry(address _userAddress) external view returns (uint16); + function addIdentityToStorage(address _userAddress, IIdentity _identity, uint16 _country) external; + function removeIdentityFromStorage(address _userAddress) external; + function modifyStoredInvestorCountry(address _userAddress, uint16 _country) external; + function modifyStoredIdentity(address _userAddress, IIdentity _identity) external; + + // role setter + function bindIdentityRegistry(address _identityRegistry) external; + function unbindIdentityRegistry(address _identityRegistry) external; + + // getter for bound IdentityRegistry role + function linkedIdentityRegistries() external view returns (address[] memory); +} +``` + +### Compliance Interface + +The Compliance contract is used to set the rules of the offering itself and ensures these rules are respected during the whole lifecycle of the token. For example, the Compliance contract will define the maximum amount of investors per country, the maximum amount of tokens per investor, and the accepted countries for the circulation of the token (using the country code corresponding to each investor in the Identity Registry). The Compliance smart contract can be either “tailor-made”, following the legal requirements of the token issuer, or can be deployed under a generic modular form, which can then add and remove external compliance `Modules` to fit the legal requirements of the token in the same way as a custom "tailor-made" contract would. + +This contract is triggered at every transaction by the Token and returns `TRUE` if the transaction is compliant with the rules of the offering and `FALSE` otherwise. + +The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of setting the Compliance parameters and binding the Compliance to a Token contract. + +A detailed description of the functions can be found in the [interfaces folder](../assets/eip-3643/interfaces/ICompliance.sol). + +```solidity +interface ICompliance { + + // events + event TokenBound(address _token); + event TokenUnbound(address _token); + + // functions + // initialization of the compliance contract + function bindToken(address _token) external; + function unbindToken(address _token) external; + + // check the parameters of the compliance contract + function isTokenBound(address _token) external view returns (bool); + function getTokenBound() external view returns (address); + + // compliance check and state update + function canTransfer(address _from, address _to, uint256 _amount) external view returns (bool); + function transferred(address _from, address _to, uint256 _amount) external; + function created(address _to, uint256 _amount) external; + function destroyed(address _from, uint256 _amount) external; +} +``` + +### Trusted Issuer's Registry Interface + +The Trusted Issuer's Registry stores the contract addresses ([IClaimIssuer](../assets/eip-3643/ONCHAINID/IClaimIssuer.sol)) of all the trusted claim issuers for a specific security token. The Identity contract ([IIdentity](../assets/eip-3643/ONCHAINID/IIdentity.sol)) of token owners (the investors) must have claims signed by the claim issuers stored in this smart contract in order to be able to hold the token. + +The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of managing this registry as per their requirements. This includes the ability to add, remove, and update the list of Trusted Issuers. + +A detailed description of the functions can be found in the [interfaces folder](../assets/eip-3643/interfaces/ITrustedIssuersRegistry.sol). + +```solidity +interface ITrustedIssuersRegistry { + + // events + event TrustedIssuerAdded(IClaimIssuer indexed trustedIssuer, uint[] claimTopics); + event TrustedIssuerRemoved(IClaimIssuer indexed trustedIssuer); + event ClaimTopicsUpdated(IClaimIssuer indexed trustedIssuer, uint[] claimTopics); + + // functions + // setters + function addTrustedIssuer(IClaimIssuer _trustedIssuer, uint[] calldata _claimTopics) external; + function removeTrustedIssuer(IClaimIssuer _trustedIssuer) external; + function updateIssuerClaimTopics(IClaimIssuer _trustedIssuer, uint[] calldata _claimTopics) external; + + // getters + function getTrustedIssuers() external view returns (IClaimIssuer[] memory); + function isTrustedIssuer(address _issuer) external view returns(bool); + function getTrustedIssuerClaimTopics(IClaimIssuer _trustedIssuer) external view returns(uint[] memory); + function getTrustedIssuersForClaimTopic(uint256 claimTopic) external view returns (IClaimIssuer[] memory); + function hasClaimTopic(address _issuer, uint _claimTopic) external view returns(bool); +} +``` + +### Claim Topics Registry Interface + +The Claim Topics Registry stores all the trusted claim topics for the security token. The Identity contract ([IIdentity](../assets/eip-3643/ONCHAINID/IIdentity.sol)) of token owners must contain claims of the claim topics stored in this smart contract. + +The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of managing this registry as per their requirements. This includes the ability to add and remove required Claim Topics. + +A detailed description of the functions can be found in the [interfaces folder](../assets/eip-3643/interfaces/IClaimTopicsRegistry.sol). + +```solidity +interface IClaimTopicsRegistry { + + // events + event ClaimTopicAdded(uint256 indexed claimTopic); + event ClaimTopicRemoved(uint256 indexed claimTopic); + + // functions + // setters + function addClaimTopic(uint256 _claimTopic) external; + function removeClaimTopic(uint256 _claimTopic) external; + + // getter + function getClaimTopics() external view returns (uint256[] memory); +} +``` + +## Rationale + +### Transfer Restrictions + +Transfers of securities can fail for a variety of reasons. This is in direct contrast to utility tokens, which generally only require the sender to have a sufficient balance. These conditions can be related to the status of an investor’s wallet, the identity of the sender and receiver of the securities (i.e., whether they have been through a KYC process, whether they are accredited or an affiliate of the issuer) or for reasons unrelated to the specific transfer but instead set at the token level (i.e., the token contract enforces a maximum number of investors or a cap on the percentage held by any single investor). For ERC-20 tokens, the `balanceOf` and `allowance` functions provide a way to check that a transfer is likely to succeed before executing the transfer, which can be executed both on-chain and off-chain. For tokens representing securities, the T-REX standard introduces a function `canTransfer` which provides a more general-purpose way to achieve this. I.e., when the reasons for failure are related to the compliance rules of the token and a function `isVerified` which allows checking the eligibility status of the identity of the investor. Transfers can also fail if the address of the sender and/or receiver is frozen, or if the free balance of the sender (total balance - frozen tokens) is lower than the amount to transfer. Ultimately, the transfer could be blocked if the token is `paused`. + +### Identity Management + +Security and compliance of transfers are enforced through the management of on-chain identities. These include: + +- Identity contract: A unique identifier for each investor, which is used to manage their identity and claims. +- Claim: Signed attestations issued by a trusted claim issuer that confirm certain attributes or qualifications of the token holders, such as their identity, location, investor status, or KYC/AML clearance. +- Identity Storage/Registry: A storage system for all Identity contracts and their associated wallets, which is used to + verify the eligibility of investors during transfers. + +### Token Lifecycle Management + +The T-REX standard provides a comprehensive framework for managing the lifecycle of security tokens. This includes the issuance of tokens, transfers between eligible investors, and the enforcement of compliance rules at every stage of the token's lifecycle. The standard also supports additional features such as token pausing and freezing, which can be used to manage the token in response to regulatory requirements or changes in the status of the token or its holders. + +### Additional Compliance Rules + +The T-REX standard supports the implementation of additional compliance rules through modular compliance. These modules can be used to enforce a wide range of rules and restrictions, such as caps on the number of investors or the percentage of tokens held by a single investor, restrictions on transfers between certain types of investors, and more. This flexibility allows issuers to tailor the compliance rules of their tokens to their specific needs and regulatory environment. + +### Inclusion of Agent-Related Functions + +The inclusion of Agent-scoped functions within the standard interfaces is deliberate. The intent is to accommodate secure and adaptable token management practices that surpass the capabilities of EOA management. We envision scenarios where the agent role is fulfilled by automated systems or smart contracts, capable of programmatically executing operational functions like minting, burning, and freezing in response to specified criteria or regulatory triggers. For example, a smart contract might automatically burn tokens to align with redemption requests in an open-ended fund, or freeze tokens associated with wallets engaged in fraudulent activities. + +Consequently, these functions are standardized to provide a uniform interface for various automated systems interacting with different ERC-3643 tokens, allowing for standardized tooling and interfaces that work across the entire ecosystem. This approach ensures that ERC-3643 remains flexible, future-proof, and capable of supporting a wide array of operational models. + +## Backwards Compatibility + +T-REX tokens should be backwards compatible with ERC-20 and ERC-173 +and should be able to interact with a [Claim Holder contract](../assets/eip-3643/ONCHAINID/IERC735.sol) to validate +the claims linked to an [Identity contract](../assets/eip-3643/ONCHAINID/IIdentity.sol). + + +## Security Considerations + +This specification has been audited by Kapersky and Hacken, and no notable security considerations were found. +While the audits were primarily focused on the specific implementation by Tokeny, they also challenged and validated the core principles of the T-REX standard. The auditing teams approval of these principles provides assurance that the standard itself is robust and does not present any significant security concerns. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/ERCSpecification/erc-6357-multicall.md b/doc/ERCSpecification/erc-6357-multicall.md new file mode 100644 index 0000000..9664ba7 --- /dev/null +++ b/doc/ERCSpecification/erc-6357-multicall.md @@ -0,0 +1,91 @@ +--- +eip: 6357 +title: Single-contract Multi-delegatecall +description: Allows an EOA to call multiple functions of a smart contract in a single transaction +author: Gavin John (@Pandapip1) +discussions-to: https://ethereum-magicians.org/t/eip-6357-single-contract-multicall/12621 +status: Last Call +last-call-deadline: 2023-11-10 +type: Standards Track +category: ERC +created: 2023-01-18 +--- + +## Abstract + +This EIP standardizes an interface containing a single function, `multicall`, allowing EOAs to call multiple functions of a smart contract in a single transaction, and revert all calls if any call fails. + +## Motivation + +Currently, in order to transfer several [ERC-721](./eip-721.md) NFTs, one needs to submit a number of transactions equal to the number of NFTs being tranferred. This wastes users' funds by requiring them to pay 21000 gas fee for every NFT they transfer. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +Contracts implementing this EIP must implement the following interface: + +```solidity +pragma solidity ^0.8.0; + +interface IMulticall { + /// @notice Takes an array of abi-encoded call data, delegatecalls itself with each calldata, and returns the abi-encoded result + /// @dev Reverts if any delegatecall reverts + /// @param data The abi-encoded data + /// @returns results The abi-encoded return values + function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results); + + /// @notice OPTIONAL. Takes an array of abi-encoded call data, delegatecalls itself with each calldata, and returns the abi-encoded result + /// @dev Reverts if any delegatecall reverts + /// @param data The abi-encoded data + /// @param values The effective msg.values. These must add up to at most msg.value + /// @returns results The abi-encoded return values + function multicallPayable(bytes[] calldata data, uint256[] values) external payable virtual returns (bytes[] memory results); +} +``` + +## Rationale + +`multicallPayable` is optional because it isn't always feasible to implement, due to the `msg.value` splitting. + +## Backwards Compatibility + +This is compatible with most existing multicall functions. + +## Test Cases + +The following JavaScript code, using the Ethers library, should atomically transfer `amt` units of an [ERC-20](./eip-20.md) token to both `addressA` and `addressB`. + +```js +await token.multicall(await Promise.all([ + token.interface.encodeFunctionData('transfer', [ addressA, amt ]), + token.interface.encodeFunctionData('transfer', [ addressB, amt ]), +])); +``` + +## Reference Implementation + +```solidity +pragma solidity ^0.8.0; + +/// Derived from OpenZeppelin's implementation +abstract contract Multicall is IMulticall { + function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + (bool success, bytes memory returndata) = address(this).delegatecall(data); + require(success); + results[i] = returndata; + } + return results; + } +} +``` + +## Security Considerations + +`multicallPayable` should only be used if the contract is able to support it. A naive attempt at implementing it could allow an attacker to call a payable function multiple times with the same ether. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/ERCSpecification/erc-7551-ewpg.md b/doc/ERCSpecification/erc-7551-ewpg.md new file mode 100644 index 0000000..2a6f348 --- /dev/null +++ b/doc/ERCSpecification/erc-7551-ewpg.md @@ -0,0 +1,277 @@ +--- +eip: 7551 +title: Crypto Security Token Interface +description: Interface for regulated security tokens with compliance checks, operator-controlled supply, freezing, forced transfers, and on-chain terms +author: Lars Olsson , Hagen Hübel (@itinance) , Markus Kluge , Andreas Berghammer , Ryan Sauge (@rya-sge) +discussions-to: https://ethereum-magicians.org/t/erc-7551-crypto-security-token-smart-contract-interface-ewpg-reworked/25477 +status: Draft +type: Standards Track +category: ERC +created: 2023-09-05 +--- + +## Abstract + +This proposal defines an interface that extends existing token standards such as [ERC-20](./eip-20.md) or [ERC-1155](./eip-1155.md) to support regulated crypto-securities. While the underlying token standard manages balances and transfers, crypto-securities require additional functionality for compliance, operator-driven supply control, and legally mandated enforcement actions. + +The interface introduces transfer-compliance checks (`canTransfer`, `canTransferFrom`) that allow wallets and exchanges to determine whether a transfer would succeed under the current regulatory or eligibility rules. It also defines operator-controlled minting and burning of tokens, aligned with the semantics of [EIP-5679](./eip-5679.md) for contract-level issuance and redemption. + +To support legal enforcement, the interface provides a `forcedTransfer` function for operator-initiated transfers without holder consent. Implementations may automatically unfreeze balances when executing such transfers, and enforcement-related events are available for auditing. + +Additional mechanisms include freezing and unfreezing portions of an account's balance, pausing standard transfers, and anchoring the legally binding issuance documents on-chain via a deterministic `termsHash`. A metadata field allows machine-readable descriptions of the security. + +## Motivation + +The compliant representation of securities on a distributed ledger network (“crypto securities”) remains one of the most prominent use cases for distributed ledger systems. Up until recent developments such activities were not always fully recognized by local securities laws. This led to different views on what information and functionality they should provide. Germany, as one of the first countries in the world, has enhanced its legal framework to fully cover the issuance of securities in electronic form on a distributed ledger network. This standard aims to capture these legal requirements and use them as a framework to define a smart contract interface that enables interactions with crypto securities on-ledger. This standard is backed by the Federal Association of Crypto Registrars and a result of its task force for standardization. + +While standards like [ERC-20](./eip-20.md) and [ERC-1155](./eip-1155.md) provide complete interfaces to interact with utility tokens, in order to represent securities in the form of a token more advanced features are necessary. This is caused by two main characteristics of crypto securities: + +* In contrast to utility tokens where transfers usually only require the sender to have a sufficient balance, for crypto securities more complex rules can apply that use other data to determine the validity of a transfer. In many cases, token holders with their respective addresses need to be eligible, for example, according to KYC/AML regulation or qualification of the investor to receive and hold tokens. +* Crypto securities need a trusted operator that is granted certain permissions such as pausing transfers or managing the token supply. This trusted operator is sometimes even recognized by local securities laws and licensed by the authorities. + +This standard should facilitate the interaction of wallet software, exchanges as well as crypto security operators with different implementations of crypto securities. For wallets and exchanges, this eases the listing of crypto securities no matter who their issuer or operator is. It also makes sure that in case of an operator becoming unavailable, other operators can step in and take over control of the token, securing the rights of token holders. + +## Specification + +The following specification describes events and functions of a token smart contract representing a crypto security. + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). + + +### General +Tokens MAY be received by externally owned accounts as well as smart contract accounts ("token holders"). An externally owned account or smart contract account MAY be granted special administrative permissions ("operator"). There MAY be more than one operator. + +### Functions +#### `getActiveBalanceOf` +This function MUST return the unfrozen balance of an account. This balance can be used by the token holder for transfers to other account addresses. + +```solidity +function getActiveBalanceOf(address tokenHolder) external view returns (uint256); +``` + +#### `getFrozenTokens` +This function MUST return the frozen balance of an account. Frozen tokens cannot be transferred using standard [ERC-20](./eip-20.md) functions. Implementations MAY support transferring frozen tokens via `forcedTransfer`. Implementations MAY also support burning frozen tokens via `burn`. This function is informational and does not define compliance behavior. + +```solidity +function getFrozenTokens(address tokenHolder) external view returns (uint256); +``` + +#### `paused` +This function MUST return `true` if token transfers are paused and MUST return `false` otherwise. When `true`, standard holder‑to‑holder transfers MUST revert and `canTransfer()` / `canTransferFrom()` MUST return `false`. The functions `mint`, `burn`, and `forcedTransfer` are not affected. + +```solidity +function paused() external view returns (bool); +``` + +#### `termsHash` +The document hash MUST deterministically represent the full document content. The hashing algorithm SHOULD be SHA-256 and MUST be exposed as `bytes32`. +A terms hash with the value `0x0` has a special meaning: As long as it is `0x0`, transfers cannot be unpaused. + + +```solidity +function termsHash() external view returns (bytes32); +``` + +#### `metaData` +This function MUST return a JSON object containing metadata about the crypto security in the form of key-value pairs. It MAY be empty. + +```solidity +function metaData() external view returns (string memory); +``` + +#### `canTransfer` +This function MUST return `true` if `from` is able to transfer `value` tokens to `to` respecting all compliance, investor eligibility and other implemented restrictions. Otherwise it MUST return `false`. +If `paused()` returns `true`, the function MUST return `false`. Balance and access-control checks are out of scope for this function. + +```solidity +function canTransfer(address from, address to, uint256 value) external view returns (bool); +``` + +#### `canTransferFrom` +This function MUST return `true` if `spender` can transfer `value` tokens from `from` to `to` respecting all compliance, investor eligibility and other implemented restrictions. Otherwise it MUST return `false`. +If `paused()` returns `true`, the function MUST return `false`. Balance and access-control checks are out of scope for this function. + +```solidity +function canTransferFrom(address spender, address from, address to, uint256 value) external view returns (bool); +``` + +#### `mint` +This function MUST increase the balance of `to` by `amount` and MUST NOT decrease the balance of any other holder. It MUST increase the token's `totalSupply` by `amount` as reported by the underlying token standard (e.g., [ERC-20](./eip-20.md) or [ERC-1155](./eip-1155.md)). + +The function MUST emit both a `Transfer` event and a `Mint` event. Ideally, the `Transfer` event is emitted by the underlying token implementation. Paused transfers SHOULD NOT prevent minting. The `data` parameter MAY be used to further document the action. + +```solidity +function mint(address to, uint256 amount, bytes calldata data) external; +``` + +#### `burn` +This function MUST reduce the balance of `tokenHolder` by `amount` without increasing the amount of tokens of any other holder. It MUST emit a `Burn` as well as a `Transfer` event. The `Transfer` event MUST contain `0x0` as the recipient account address. The function MUST revert if `tokenHolder`'s balance is less than `amount` (including frozen tokens). If frozen balance is used, a `TokensUnfrozen` event SHOULD be emitted. Paused transfers SHOULD NOT prevent burning tokens. The `data` parameter MAY be used to further document the action. + +```solidity +function burn(address tokenHolder, uint256 amount, bytes calldata data) external; +``` + +#### `forcedTransfer` +This function MUST transfer `value` tokens from `account` to `to` without requiring the consent of `account`. The function MUST revert if `account`'s balance is less than `value` (including frozen tokens). The function MUST emit a standard `Transfer` event and SHOULD emit a `ForcedTransfer` event. If frozen balance is used, a `TokensUnfrozen` event SHOULD be emitted. The `data` parameter MAY be used to further document the action. + +```solidity +function forcedTransfer(address account, address to, uint256 value, bytes calldata data) external returns (bool); +``` + +#### `freezePartialTokens` +This function MUST freeze `amount` tokens of `account`. Frozen tokens cannot be transferred to other accounts. The function MUST emit a `TokensFrozen` event. The function MUST revert if `account`'s active balance is less than `amount` (excluding already frozen tokens). The `data` parameter MAY be used to further document the action. + +```solidity +function freezePartialTokens(address account, uint256 amount, bytes calldata data) external; +``` + +#### `unfreezePartialTokens` +This function MUST unfreeze `amount` tokens of `account`. The function MUST emit a `TokensUnfrozen` event. The function MUST revert if `account`'s frozen balance is less than `amount`. The `data` parameter MAY be used to further document the action. + +```solidity +function unfreezePartialTokens(address account, uint256 amount, bytes calldata data) external; +``` + +#### `pause` +Calling this function MUST pause all token transfers. The function MUST revert if the contract is already paused. + +```solidity +function pause() external; +``` + +#### `unpause` +Calling this function MUST unpause all token transfers. +The function MUST revert if the contract is not currently paused. +The function MUST also revert if `termsHash()` returns `0x0`. In all cases, `unpause()` MUST fail unless a non-zero document hash is set on-chain. + +```solidity +function unpause() external; +``` + +#### `setTerms` +This function MUST update the `termsHash` value. `_hash` MUST deterministically represent the full document content. The hashing algorithm SHOULD be SHA-256. It SHOULD emit a `Terms(_hash, _uri)` event. + +```solidity +function setTerms(bytes32 _hash, string calldata _uri) external; +``` + +#### `setMetaData` +This function MUST update the `metaData` value. `_metadata` MUST be a JSON object containing metadata about the crypto security in the form of key-value pairs. It MAY be empty. + +```solidity +function setMetaData(string calldata _metadata) external; +``` + +### Events +Alternative event names with equivalent semantics (e.g., a dedicated forced-transfer event vs. a generic Transfer) are allowed; however, a Transfer event MUST be emitted on state-changing token movements. + +#### `Mint` +This event MUST be emitted when new tokens are issued and the token's `totalSupply` increases. + +```solidity +event Mint(address indexed minter, address indexed account, uint256 value, bytes data); +``` + +#### `Burn` +This event MUST be triggered when tokens are destroyed of a `tokenHolder`. + +```solidity +event Burn(address indexed burner, address indexed account, uint256 value, bytes data); +``` + +#### `ForcedTransfer` +This event SHOULD be triggered when a forced transfer is executed via `forcedTransfer(...)`. + +```solidity +event ForcedTransfer(address indexed operator, address indexed from, address indexed to, uint256 value, bytes data); +``` + +#### `TokensFrozen` +This event SHOULD be triggered if tokens are frozen for an account. + +```solidity +event TokensFrozen(address indexed account, uint256 value, bytes data); +``` + +#### `TokensUnfrozen` +This event SHOULD be triggered if tokens are unfrozen for an account. + +```solidity +event TokensUnfrozen(address indexed account, uint256 value, bytes data); +``` + +#### `Terms` +This event SHOULD be triggered when the `termsHash` value (and/or the associated URI) is updated. + +```solidity +event Terms(bytes32 hash, string uri); +``` + +#### `MetaData` +This event SHOULD be triggered when the `metaData` value is updated. + +```solidity +event MetaData(string newMetaData); +``` + +### Interface +```solidity +interface IERC7551 { + // Events + event Mint(address indexed minter, address indexed account, uint256 value, bytes data); + event Burn(address indexed burner, address indexed account, uint256 value, bytes data); + event ForcedTransfer(address indexed operator, address indexed from, address indexed to, uint256 value, bytes data); + event TokensFrozen(address indexed account, uint256 value, bytes data); + event TokensUnfrozen(address indexed account, uint256 value, bytes data); + event Terms(bytes32 hash, string uri); + event MetaData(string newMetaData); + + // View functions + function getActiveBalanceOf(address tokenHolder) external view returns (uint256); + function getFrozenTokens(address tokenHolder) external view returns (uint256); + function paused() external view returns (bool); + function termsHash() external view returns (bytes32); + function metaData() external view returns (string memory); + function canTransfer(address from, address to, uint256 value) external view returns (bool); + function canTransferFrom(address spender, address from, address to, uint256 value) external view returns (bool); + // Operator functions + function mint(address to, uint256 amount, bytes calldata data) external; + function burn(address tokenHolder, uint256 amount, bytes calldata data) external; + function forcedTransfer(address account, address to, uint256 value, bytes calldata data) external returns (bool); + function freezePartialTokens(address account, uint256 amount, bytes calldata data) external; + function unfreezePartialTokens(address account, uint256 amount, bytes calldata data) external; + function pause() external; + function unpause() external; + function setTerms(bytes32 _hash, string calldata _uri) external; + function setMetaData(string calldata _metadata) external; +} +``` + +## Rationale + +This standard is the result of the standardization working group of the German Federal Association of Crypto Registrars. It's based on the smart contract implementations of its members. The design draws conceptual inspiration from a set of security-token specifications published by the Security Token Roundtable outside the official Ethereum EIP process. These documents influenced early security-token implementations but were never formalized as official ERCs. We also acknowledge [ERC-3643](./eip-3643.md) as a related standard. Both were considered as alternatives to drafting this standard, but we decided to propose an alternative standard instead of using them directly. + +The reasons include: +- Mandatory identity layer (onchainID): [ERC-3643](./eip-3643.md) tightly couples transfer compliance with a specific decentralized identity framework. This limits flexibility and imposes implementation constraints not aligned with the regulatory environment in certain jurisdictions, such as Germany. + +- Access control model: [ERC-3643](./eip-3643.md) introduces constraints through the Agent role, which reduces flexibility for implementing alternative RBAC systems. + +- Completeness: Some of the features defined by the Roundtable and [ERC-3643](./eip-3643.md) address very specific use cases that are not common to all association members. To support compatibility, we deliberately kept the same function names as [ERC-3643](./eip-3643.md) whenever possible. In contrast, this ERC aims to define a minimal and flexible foundational interface that can be combined with the underlying token standard (e.g. [ERC-20](./eip-20.md) or [ERC-1155](./eip-1155.md)), operator permission management, and compliance modules. + +This standard should not be understood to be a guideline for developing security token implementations. For a full security token implementation, we expect this standard to be combined with the underlying token itself (e.g. based on [ERC-20](./eip-20.md) or [ERC-1155](./eip-1155.md)), permission management and authorization logic for operators, different mechanisms to determine the compliance of a specific token transfer, as well as mechanisms to upgrade the token smart contract logic. In contrast, this interface standard describes minimum requirements for the token smart contract representing a crypto security. + +These functions follow the standard OpenZeppelin Pausable pattern and are compatible with existing Ethereum infrastructure. + +**Informative note:** Some deployments may prefer not to gate `unpause()` on the presence of an on‑chain terms document. This ERC mandates the `termsHash != 0x0` gate to ensure there is a tamper‑evident, immutable reference to the issuance documents before enabling standard transfers. Jurisdictions that do not require an on‑chain linkage can still deploy the interface and delay setting `termsHash`; until then, standard holder‑to‑holder transfers remain paused while `mint`, `burn`, and `forcedTransfer` remain available. + +## Security Considerations + +The standard specifications don't include requirements for permission management of operators. Implementations SHOULD make sure that the operator functions can only be executed with sufficient authorization. + +In addition, to be able to fix security issues, the token smart contract's logic SHOULD be upgradable by the operator or another account with sufficient authorization. + +The specification puts a lot of trust into the operators because they can manage the token supply and even force transfer token amounts. Therefore, entities managing operator accounts must ensure that they use secure off-chain infrastructures to manage and interact with the smart contract implementation. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/ERCSpecification/erc-7943-uRWA.md b/doc/ERCSpecification/erc-7943-uRWA.md new file mode 100644 index 0000000..c10c08d --- /dev/null +++ b/doc/ERCSpecification/erc-7943-uRWA.md @@ -0,0 +1,462 @@ +# ERC-7943: uRWA - Universal Real World Asset Interface + +> Source: https://eips.ethereum.org/EIPS/eip-7943 + + + +### Interfaces for common base tokens defining compliance checks, transfer controls, and enforcement actions for Real World Assets (RWAs). + + +| Authors | Dario Lo Buglio ([@xaler5](https://github.com/xaler5)), Tino Martinez Molina ([@tinom9](https://github.com/tinom9)), Mihai Colceriu ([@mihaic195](https://github.com/mihaic195)) | +| --- | --- | +| Created | 2025-06-10 | +| Last Call Deadline | 2026-01-30 | +| Requires | [EIP-165](https://eips.ethereum.org/EIPS/eip-165) | + + [This EIP is in the process of being peer-reviewed. If you are interested in this EIP, please participate using this discussion link.](https://ethereum-magicians.org/t/erc-universal-rwa-interface/23972) + + + + + +## Table of Contents + + +- [Abstract](#abstract) +- [Motivation](#motivation) +- [Specification](#specification) + + - [canTransact, canTransfer and getFrozenTokens](#cantransact-cantransfer-and-getfrozentokens) + - [forcedTransfer](#forcedtransfer) + - [setFrozenTokens](#setfrozentokens) + - [Additional Specifications](#additional-specifications) +- [Rationale](#rationale) + + - [Extensibility](#extensibility) + - [Notes on naming](#notes-on-naming) +- [Backwards Compatibility](#backwards-compatibility) +- [Reference Implementation](#reference-implementation) +- [Security Considerations](#security-considerations) +- [Copyright](#copyright) + + + + +## [](#abstract) Abstract + +This EIP proposes the Universal RWA (uRWA) standard, a set of interfaces for tokenized Real World Assets (RWAs) such as securities, real estate, commodities, or other physical/financial assets on the blockchain. + +Real World Assets often require regulatory compliance features not found in standard tokens, including the ability to freeze assets, perform enforcement transfers for legal compliance, and restrict transfers to authorized users. The uRWA standard extends common token standards like [ERC-20](https://eips.ethereum.org/EIPS/eip-20), [ERC-721](https://eips.ethereum.org/EIPS/eip-721) or [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155) by introducing essential compliance functions while remaining minimal and not opinionated about specific implementation details. + +This enables DeFi protocols and applications to interact with tokenized real-world assets in a standardized way, knowing they can check transfer permissions, whether users are allowed to interact, handle frozen assets appropriately, and integrate with compliant RWA tokens regardless of the underlying asset type or internal compliance logic. It also adopts [ERC-165](https://eips.ethereum.org/EIPS/eip-165) for introspection. + + +​ +## [](#motivation) Motivation + +Real World Assets (RWAs) represent a significant opportunity to bridge traditional finance and decentralized finance (DeFi). By tokenizing assets like real estate, corporate bonds, commodities, art, or securities, we can unlock benefits such as fractional ownership, programmable compliance, enhanced liquidity through secondary markets for traditionally illiquid assets and integration with decentralized protocols. + +However, tokenizing real world assets introduces regulatory requirements often absent in purely digital assets, such as allowlists for users, transfer restrictions, asset freezing, or law enforcement rules. Existing token standards like [ERC-20](https://eips.ethereum.org/EIPS/eip-20), [ERC-721](https://eips.ethereum.org/EIPS/eip-721) and [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155) lack the inherent structure to address these compliance needs directly within the standard itself. + +Attempts at defining universal RWA standards historically imposed unnecessary complexity and gas overhead for simpler use cases that do not require the full spectrum of features like granular role-based access control, mandatory on-chain whitelisting, specific on-chain identity solutions, or metadata handling solutions mandated by the standard. + +Additionally, the broad spectrum of RWA classes inherently suggests the need to move away from a one-size-fits-all solution. This means a minimalistic approach, unopinionated features list, and maximally compatible design have been kept in mind as design goals. + +The uRWA standard seeks a more refined balance by defining an essential interface, establishing a common ground for interaction regarding compliance and control, without dictating the underlying implementation mechanisms. This allows core token implementations to remain lean while providing standard functions for RWA-specific interactions. + +The final goal is to build composable DeFi around RWAs, providing the same interface when dealing with compliance and regulation. + + +​ +## [](#specification) Specification + +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHOULD”, and “MAY” in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +The following defines the standard interfaces for an [ERC-7943](https://eips.ethereum.org/EIPS/eip-7943) token contract, which MUST extend from one base token interface such as [ERC-20](https://eips.ethereum.org/EIPS/eip-20), [ERC-721](https://eips.ethereum.org/EIPS/eip-721) or [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155): + +``` +/// @notice Interface for ERC-20 based implementations. +interface IERC7943Fungible is IERC165 { + /// @notice Emitted when tokens are taken from one address and transferred to another. + /// @param from The address from which tokens were taken. + /// @param to The address to which seized tokens were transferred. + /// @param amount The amount seized. + event ForcedTransfer(address indexed from, address indexed to, uint256 amount); + + /// @notice Emitted when `setFrozenTokens` is called, changing the frozen `amount` of tokens for `account`. + /// @param account The address of the account whose tokens are being frozen. + /// @param amount The amount of tokens frozen after the change. + event Frozen(address indexed account, uint256 amount); + + /// @notice Error reverted when an account is not allowed to transact. + /// @param account The address of the account which is not allowed for transfers. + error ERC7943CannotTransact(address account); + + /// @notice Error reverted when a transfer is not allowed according to internal rules. + /// @param from The address from which tokens are being sent. + /// @param to The address to which tokens are being sent. + /// @param amount The amount sent. + error ERC7943CannotTransfer(address from, address to, uint256 amount); + + /// @notice Error reverted when a transfer is attempted from `account` with an `amount` less than or equal to its balance, but greater than its unfrozen balance. + /// @param account The address holding the tokens. + /// @param amount The amount being transferred. + /// @param unfrozen The amount of tokens that are unfrozen and available to transfer. + error ERC7943InsufficientUnfrozenBalance(address account, uint256 amount, uint256 unfrozen); + + /// @notice Takes tokens from one address and transfers them to another. + /// @dev Requires specific authorization. Used for regulatory compliance or recovery scenarios. + /// @param from The address from which `amount` is taken. + /// @param to The address that receives `amount`. + /// @param amount The amount to force transfer. + /// @return result True if the transfer executed correctly. Reverts on failure. + function forcedTransfer(address from, address to, uint256 amount) external returns(bool result); + + /// @notice Changes the frozen status of `amount` tokens belonging to `account`. + /// This overwrites the current value, similar to an `approve` function. + /// @dev Requires specific authorization. Frozen tokens cannot be transferred by the account. + /// @param account The address of the account whose tokens are to be frozen. + /// @param amount The amount of tokens to freeze. It can be greater than account balance. + /// @return result True if the freezing executed correctly. Reverts on failure. + function setFrozenTokens(address account, uint256 amount) external returns(bool result); + + /// @notice Checks if a specific account is allowed to transact according to token rules. + /// @dev This is often used for allowlist/KYC/KYB/AML checks. + /// @param account The address to check. + /// @return allowed True if the account is allowed, false otherwise. + function canTransact(address account) external view returns (bool allowed); + + /// @notice Checks the frozen status/amount. + /// @param account The address of the account. + /// @dev It could return an amount higher than the account's balance. + /// @return amount The amount of tokens currently frozen for `account`. + function getFrozenTokens(address account) external view returns (uint256 amount); + + /// @notice Checks if a transfer is currently possible according to token rules. It enforces validations on the frozen tokens. + /// @dev This can involve checks like allowlists, blocklists, transfer limits and other policy-defined restrictions. + /// @param from The address sending tokens. + /// @param to The address receiving tokens. + /// @param amount The amount being transferred. + /// @return allowed True if the transfer is allowed, false otherwise. + function canTransfer(address from, address to, uint256 amount) external view returns (bool allowed); +} + +/// @notice Interface for ERC-721 based implementations. +interface IERC7943NonFungible is IERC165 { + /// @notice Emitted when `tokenId` is taken from one address and transferred to another. + /// @param from The address from which `tokenId` is taken. + /// @param to The address to which seized `tokenId` is transferred. + /// @param tokenId The ID of the token being transferred. + event ForcedTransfer(address indexed from, address indexed to, uint256 indexed tokenId); + + /// @notice Emitted when `setFrozenTokens` is called, changing the frozen status of `tokenId` for `account`. + /// @param account The address of the account whose `tokenId` is subjected to freeze/unfreeze. + /// @param tokenId The ID of the token subjected to freeze/unfreeze. + /// @param frozenStatus Whether `tokenId` has been frozen or unfrozen. + event Frozen(address indexed account, uint256 indexed tokenId, bool indexed frozenStatus); + + /// @notice Error reverted when an account is not allowed to transact. + /// @param account The address of the account which is not allowed for transfers. + error ERC7943CannotTransact(address account); + + /// @notice Error reverted when a transfer is not allowed according to internal rules. + /// @param from The address from which tokens are being sent. + /// @param to The address to which tokens are being sent. + /// @param tokenId The id of the token being sent. + error ERC7943CannotTransfer(address from, address to, uint256 tokenId); + + /// @notice Error reverted when a transfer is attempted from `account` with a `tokenId` which has been previously frozen. + /// @param account The address holding the token with `tokenId`. + /// @param tokenId The ID of the token being frozen and unavailable to be transferred. + error ERC7943InsufficientUnfrozenBalance(address account, uint256 tokenId); + + /// @notice Takes `tokenId` from one address and transfers it to another. + /// @dev Requires specific authorization. Used for regulatory compliance or recovery scenarios. + /// @param from The address from which `tokenId` is taken. + /// @param to The address that receives `tokenId`. + /// @param tokenId The ID of the token being transferred. + /// @return result True if the transfer executed correctly. Reverts on failure. + function forcedTransfer(address from, address to, uint256 tokenId) external returns(bool result); + + /// @notice Changes the frozen status of `tokenId` belonging to an `account`. + /// This overwrites the current value, similar to an `approve` function. + /// @dev Requires specific authorization. Frozen tokens cannot be transferred by the account. + /// @param account The address of the account whose tokens are to be frozen. + /// @param tokenId The ID of the token to freeze. + /// @param frozenStatus Whether `tokenId` is being frozen or not. + /// @return result True if the freezing executed correctly. Reverts on failure. + function setFrozenTokens(address account, uint256 tokenId, bool frozenStatus) external returns(bool result); + + /// @notice Checks if a specific account is allowed to transact according to token rules. + /// @dev This is often used for allowlist/KYC/KYB/AML checks. + /// @param account The address to check. + /// @return allowed True if the account is allowed, false otherwise. + function canTransact(address account) external view returns (bool allowed); + + /// @notice Checks the frozen status of a specific `tokenId`. + /// @dev It could return true even if account does not hold the token. + /// @param account The address of the account. + /// @param tokenId The ID of the token. + /// @return frozenStatus Whether `tokenId` is currently frozen for `account`. + function getFrozenTokens(address account, uint256 tokenId) external view returns (bool frozenStatus); + + /// @notice Checks if a transfer is currently possible according to token rules. It enforces validations on the frozen tokens. + /// @dev This can involve checks like allowlists, blocklists, transfer limits and other policy-defined restrictions. + /// @param from The address sending tokens. + /// @param to The address receiving tokens. + /// @param tokenId The ID of the token being transferred. + /// @return allowed True if the transfer is allowed, false otherwise. + function canTransfer(address from, address to, uint256 tokenId) external view returns (bool allowed); +} + +/// @notice Interface for ERC-1155 based implementations. +interface IERC7943MultiToken is IERC165 { + /// @notice Emitted when tokens are taken from one address and transferred to another. + /// @param from The address from which tokens were taken. + /// @param to The address to which seized tokens were transferred. + /// @param tokenId The ID of the token being transferred. + /// @param amount The amount seized. + event ForcedTransfer(address indexed from, address indexed to, uint256 indexed tokenId, uint256 amount); + + /// @notice Emitted when `setFrozenTokens` is called, changing the frozen `amount` of `tokenId` tokens for `account`. + /// @param account The address of the account whose tokens are being frozen. + /// @param tokenId The ID of the token being frozen. + /// @param amount The amount of tokens frozen after the change. + event Frozen(address indexed account, uint256 indexed tokenId, uint256 amount); + + /// @notice Error reverted when an account is not allowed to transact. + /// @param account The address of the account which is not allowed for transfers. + error ERC7943CannotTransact(address account); + + /// @notice Error reverted when a transfer is not allowed according to internal rules. + /// @param from The address from which tokens are being sent. + /// @param to The address to which tokens are being sent. + /// @param tokenId The id of the token being sent. + /// @param amount The amount sent. + error ERC7943CannotTransfer(address from, address to, uint256 tokenId, uint256 amount); + + /// @notice Error reverted when a transfer is attempted from `account` with an `amount` of `tokenId` less than or equal to its balance, but greater than its unfrozen balance. + /// @param account The address holding the `amount` of `tokenId` tokens. + /// @param tokenId The ID of the token being transferred. + /// @param amount The amount of `tokenId` tokens being transferred. + /// @param unfrozen The amount of tokens that are unfrozen and available to transfer. + error ERC7943InsufficientUnfrozenBalance(address account, uint256 tokenId, uint256 amount, uint256 unfrozen); + + /// @notice Takes tokens from one address and transfers them to another. + /// @dev Requires specific authorization. Used for regulatory compliance or recovery scenarios. + /// @param from The address from which `amount` is taken. + /// @param to The address that receives `amount`. + /// @param tokenId The ID of the token being transferred. + /// @param amount The amount to force transfer. + /// @return result True if the transfer executed correctly. Reverts on failure. + function forcedTransfer(address from, address to, uint256 tokenId, uint256 amount) external returns(bool result); + + /// @notice Changes the frozen status of `amount` of `tokenId` tokens belonging to an `account`. + /// This overwrites the current value, similar to an `approve` function. + /// @dev Requires specific authorization. Frozen tokens cannot be transferred by the account. + /// @param account The address of the account whose tokens are to be frozen. + /// @param tokenId The ID of the token to freeze. + /// @param amount The amount of tokens to freeze. It can be greater than account balance. + /// @return result True if the freezing executed correctly. Reverts on failure. + function setFrozenTokens(address account, uint256 tokenId, uint256 amount) external returns(bool result); + + /// @notice Checks if a specific account is allowed to transact according to token rules. + /// @dev This is often used for allowlist/KYC/KYB/AML checks. + /// @param account The address to check. + /// @return allowed True if the account is allowed, false otherwise. + function canTransact(address account) external view returns (bool allowed); + + /// @notice Checks the frozen status/amount of a specific `tokenId`. + /// @dev It could return an amount higher than the account's balance. + /// @param account The address of the account. + /// @param tokenId The ID of the token. + /// @return amount The amount of `tokenId` tokens currently frozen for `account`. + function getFrozenTokens(address account, uint256 tokenId) external view returns (uint256 amount); + + /// @notice Checks if a transfer is currently possible according to token rules. It enforces validations on the frozen tokens. + /// @dev This can involve checks like allowlists, blocklists, transfer limits and other policy-defined restrictions. + /// @param from The address sending tokens. + /// @param to The address receiving tokens. + /// @param tokenId The ID of the token being transferred. + /// @param amount The amount being transferred. + /// @return allowed True if the transfer is allowed, false otherwise. + function canTransfer(address from, address to, uint256 tokenId, uint256 amount) external view returns (bool allowed); +} +``` + + +​ +### [](#cantransact-cantransfer-and-getfrozentokens) `canTransact`, `canTransfer` and `getFrozenTokens` + +These provide views into the implementing contract’s compliance, transfer policy logic and freezing status. These functions: + +- MUST NOT revert. +- MUST NOT change the storage of the contract. +- MAY depend on context (e.g., current timestamp, block number or `msg.sender`). +- The `canTransfer` + + - MUST validate that the `amount` being transferred doesn’t exceed the unfrozen amount (which is the difference between the current balance and the frozen balance). + - MUST perform a `canTransact` check on the `from` and `to` parameters. An important documentation note is that [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) doesn’t perform a `canTransact` check within `canTransfer` as required. + - MUST return false in general, if any permissioned rule would prevent a given transfer from succeeding. A transfer refers to any operation that emits the token’s canonical transfer event. A permissioned check can be a pausing mechanism, a call to `canTransact` or anything else that requires privileged actors. +- `getFrozenTokens` will return the absolute frozen amount, which MAY exceed the account’s current balance. In [ERC-721](https://eips.ethereum.org/EIPS/eip-721) tokens, it MAY return true even if the account does not hold the token. + + + +### [](#forcedtransfer) `forcedTransfer` + +This function provides a standard mechanism for forcing a transfer from a `from` to a `to` address. The function: + +- MUST directly manipulate balances or ownership to transfer the asset from `from` to `to` either by transferring or burning from `from` and minting to `to`. +- MUST be restricted in access. +- MUST perform necessary validation checks (e.g., sufficient balance/ownership of a specific token). +- MUST emit both the standard transfer event (from the base standard) and the `ForcedTransfer` event. +- In single-party permissioned contexts: + + - It MAY bypass the `canTransfer` checks. If this happens, and the transfer involves tokens that are currently counted as frozen, it MUST unfreeze the assets first and emit a `Frozen` event before the underlying base token transfer event reflecting the change. Having the unfrozen amount changed before the actual transfer is critical for tokens that might be susceptible to reentrancy attacks doing external checks on recipients as it is the case for [ERC-721](https://eips.ethereum.org/EIPS/eip-721) and [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155) tokens. + - It SHOULD at least perform a `canTransact` check on the `to` parameter to ensure compliance. +- In multi-party permissioned contexts: + + - It SHOULD perform the `canTransfer` checks and SHOULD NOT bypass the frozen constraints. +- MUST revert in cases where validations and/or `canTransact` checks return false or fail. + + + +### [](#setfrozentokens) `setFrozenTokens` + +It provides a way to freeze or unfreeze assets held by a specific account. This is useful for temporary lock mechanisms. This function: + +- MUST emit the `Frozen` event. +- MUST be restricted in access. +- MUST allow freezing more assets than those held. This allows for future balances withholding. +- MUST revert in cases of logical issues or validation checks failure. + + + +### [](#additional-specifications) Additional Specifications + +The contract MUST implement the [ERC-165](https://eips.ethereum.org/EIPS/eip-165) `supportsInterface` function and MUST return true for the `bytes4` value (representing the `interfaceId`): + +- `0x29388973` for the fungible interface. +- `0xa8fdc849` for the non-fungible interface. +- `0x5627c61a` for the multi token interface. + +Implementations of these interfaces MUST implement the necessary functions of their chosen base standard (e.g., [ERC-20](https://eips.ethereum.org/EIPS/eip-20) for the fungible interface, [ERC-721](https://eips.ethereum.org/EIPS/eip-721) for the non-fungible interface and [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155) for the multi token interface) and MUST also restrict access to sensitive functions like `forcedTransfer` and `setFrozenTokens` using an appropriate access control mechanism (e.g., `onlyOwner`, Role-Based Access Control). The specific mechanism is NOT mandated by this interface standard. + +Implementations MUST ensure their transfer methods exhibit the following behavior: + +- **Public transfers** (`transfer`, `transferFrom`, `safeTransferFrom`, etc.) MUST NOT succeed in cases in which `canTransfer` or `canTransact` would return `false` for either one or both `from` and `to` addresses. +- **Minting** in permissionless contexts (e.g., public `mint` functions) MUST NOT succeed for accounts where `canTransact` on the recipient would return `false`. In permissioned contexts (e.g., authorized minting by privileged roles), minting SHOULD respect `canTransact` checks on the recipient, though implementations MAY bypass these checks when necessary for operational or compliance reasons. +- **Burning** in permissionless contexts (e.g., public `burn` functions) MUST respect `canTransfer` check, MUST respect the `canTransact` check on the token holder and MUST NOT allow burning more assets than the unfrozen amount. In permissioned contexts (e.g., authorized burning by privileged roles), burning MAY succeed for accounts where `canTransact` on the token holder would return `false`, and MAY burn more assets than the unfrozen amount, in which case the contract MUST update the frozen status accordingly and emit a `Frozen` event before the underlying base token transfer event. + +The `ERC7943CannotTransact`/`ERC7943CannotTransfer` errors MAY be used as a general revert mechanism whenever internal calls to `canTransact`/`canTransfer` return false. They MAY be replaced by more specific errors depending on the custom checks performed inside those calls, or simply not used. + +In general, the standard prioritizes error specificity, meaning that specific errors such as `ERC7943InsufficientUnfrozenBalance` SHOULD be thrown when applicable. The `ERC7943InsufficientUnfrozenBalance` error SHOULD be triggered when a transfer is attempted from `account` with an `amount` less than or equal to its balance, but greater than its unfrozen balance or with a `tokenId` which is currently frozen. If the `amount` is greater than the whole balance or the `tokenId` is not owned by the `account`, unrelated from the frozen amount, more specific errors from the base standard SHOULD be used instead. + + +​ +## [](#rationale) Rationale + +- **Minimalism:** Defines only the essential functions (`forcedTransfer`, `setFrozenTokens`, `canTransact`, `canTransfer`, `getFrozenTokens`) and associated events/errors needed for common RWA compliance and control patterns, avoiding mandated complexity or opinionated features. The reason to introduce specific errors (`ERC7943CannotTransact`, `ERC7943CannotTransfer` and `ERC7943InsufficientUnfrozenBalance`) is to provide completeness with the introduced functionalities (`canTransact`, `canTransfer` and `getFrozenTokens`). As dictated in the specifications, error specificity is prioritized, leaving space for implementations to accommodate more explicit errors. Regarding the events `Frozen` and `ForcedTransfer`, the reason for their existence is to signal *uncommon* transfers (like in `forcedTransfer`) but also to help off-chain indexers to correctly keep track and account for asset seizures and freezing. As mentioned in the specifications, the order in which these events are emitted in relation to the base token contract events is important in practice and merits special attention. +- **Flexible compliance:** Provides standard view functions (`canTransact`, `canTransfer`, `getFrozenTokens`) for compliance checks without dictating *how* those checks are implemented internally by the token contract. This allows diverse compliance strategies. +- **Compatibility:** Designed as an interface layer compatible with existing base standards like [ERC-20](https://eips.ethereum.org/EIPS/eip-20), [ERC-721](https://eips.ethereum.org/EIPS/eip-721) and [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155). Implementations extend from [ERC-7943](https://eips.ethereum.org/EIPS/eip-7943) alongside their base standard interface. Additionally, with the adopted naming conventions, automatic backward compatibility is achieved with already existing standards like [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) and [ERC-7518](https://eips.ethereum.org/EIPS/eip-7518). +- **Essential enforcement rules:** Includes `forcedTransfer` and `setFrozenTokens` as standard functions, acknowledging their importance for regulatory enforcement in the RWA space, distinct from standard transfers. Mandates access control for this sensitive function. To maintain a lean EIP, a single `setFrozenTokens` function (which overwrites the frozen asset quantity) and one `Frozen` event were favored over distinct `freeze`/`unfreeze` functions and events. +- **[ERC-165](https://eips.ethereum.org/EIPS/eip-165):** Ensures implementing contracts can signal support for this interface. + +As an example, an AMM pool or a lending protocol can integrate with [ERC-7943](https://eips.ethereum.org/EIPS/eip-7943) based [ERC-20](https://eips.ethereum.org/EIPS/eip-20) tokens by calling `canTransact` or `canTransfer` to handle these assets in a compliant manner. Enforcement actions like `forcedTransfer` and `setFrozenTokens` can either be called by third party entities or be integrated by external protocols to allow for automated and programmable compliance. Users can then expand these tokens with additional features to fit the specific needs of individual asset types, either with on-chain identity systems, historical balances tracking for dividend distributions, semi-fungibility with token metadata, and other custom functionalities. + + +​ +### [](#extensibility) Extensibility + +While this ERC provides the necessary primitives for regulated assets, any additional feature can be added through extensions. Few examples: + +1) If for any administrative function like `setFrozenTokens` it is necessary to attach a proof to the call, the contract can have a function that batches operations like: + +``` +contract TokenWithLegalProofs is IERC7943MultiToken { + ... + + function setFrozenTokensWithProof(address account, uint256 tokenId, uint256 amount, bytes calldata legalProof) external onlyOwner returns(bool result) { + /// do anything with `legalProof` + return setFrozenTokens(account, tokenId, amount); + } +} +``` + +2) Since the `setFrozenTokens` function overwrites the absolute frozen amount and given the fact that the standard allows for multiple privileged accounts, some race-conditions might happen. If that’s the case, one can build an extension function that works with expected values of amounts frozen, like: + +``` +function setFrozenTokensIf(address account, uint256 expectedPrev, uint256 newAmount) external onlyOwner returns(bool result) { + require(frozenTokens[account] == expectedPrev, ERC7943ExpectedValueMismatch(expectedPrev, frozenTokens[account])); + return setFrozenTokens(account,newAmount); +} +``` + +Alternatively, another solution can be using delta amounts: + +``` +function setFrozenTokensDelta(address account, int256 deltaAmount) external onlyOwner returns(bool result) { + uint256 actualValue = frozenTokens[account]; + if(deltaAmount >= 0) actualValue += uint256(deltaAmount); + else { + uint256 sub = uint256(-deltaAmount); + require(sub <= actualValue, ERC7943ExpectedValueMismatch(sub, actualValue)); + actualValue -= sub; + } + return setFrozenTokens(account, actualValue); +} +``` + +*Note* These helpers reduce accidental overwrites and expand in functionalities but do not prevent same-block conflicting updates or mempool ordering races by different privileged actors. + +3) Developers can also perform several operations through the use of `multicall` patterns similar to the one defined in [ERC-6357](https://eips.ethereum.org/EIPS/eip-6357) so that a mix of the given primitives with additional features can be batched in one transaction: + +``` +contract ERC7943Fungible is IERC7943Fungible, Multicall { + // Now any combination of `setFrozenTokens`/`forcedTransfer` + // coupled with other functionalities like the ones to blacklist/whitelist users + // can be submitted in one transaction through the use of `multicall` function +} +``` + +4) Functionalities like pausability can be added on top, either through the use of modifiers or directly within functions implementations: + +``` +function canTransfer(address from, address to, uint256 amount) external view returns (bool allowed) { + if(paused()) return allowed; + // ... other checks + }; +} +``` + + +​ +### [](#notes-on-naming) Notes on naming + +The naming conventions in this ERC were carefully chosen to establish clarity and semantic consistency within the broader RWA ecosystem while maintaining neutrality and broad applicability. + +- **`forcedTransfer`**: This term was selected for its neutrality. While names like *confiscation*, *revocation*, or *recovery* describe specific motivations, `forcedTransfer` purely denotes the direct action of transferring assets, irrespective of the underlying reason. `forcedTransfer` was preferred over `forceTransfer` to maintain backward compatibility with [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643). +- **`canTransfer`**: This name was preferred over `isTransferAllowed` for consistency with established RWA standards including [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) and [ERC-7518](https://eips.ethereum.org/EIPS/eip-7518). This alignment promotes interoperability and reduces cognitive overhead when working across different RWA tokens. +- **`setFrozenTokens` / `getFrozenTokens`**: These names were chosen for managing transfer restrictions and align with [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) naming patterns. *Frozen* was also selected for its general applicability to both fungible (amount-based) and non-fungible (status-based) assets, as terms like *amount* or *asset(s)* might not be universally fitting. +- **`ERC7943InsufficientUnfrozenBalance`**: Discussions around *insufficient* being similar to *unavailable* arose, where *unavailable* might have better suggested a temporal condition like a freezing status. However, the term *available*/*unavailable* was also overlapping with *frozen*/*unfrozen* creating more confusion and duality. Finally, coupling *insufficient* with the specified *unfrozen balance* better represents the domain, prefix and subject of the error, according to [ERC-6093](https://eips.ethereum.org/EIPS/eip-6093) guidelines. + + + +## [](#backwards-compatibility) Backwards Compatibility + +This EIP defines a new interface standard and does not alter existing ones like [ERC-20](https://eips.ethereum.org/EIPS/eip-20), [ERC-721](https://eips.ethereum.org/EIPS/eip-721) and [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155). Standard wallets and explorers can interact with the base token functionality of implementing contracts, subject to the rules enforced by that contract’s implementation of `canTransact`, `canTransfer` and `getFrozenTokens` functions. Full support for the [ERC-7943](https://eips.ethereum.org/EIPS/eip-7943) functions requires explicit integration. + + +​ +## [](#reference-implementation) Reference Implementation + +Reference implementations of uRWA for [ERC-20](https://eips.ethereum.org/assets/eip-7943/contracts/uRWA20.sol), [ERC-721](https://eips.ethereum.org/assets/eip-7943/contracts/uRWA721.sol) and [ERC-1155](https://eips.ethereum.org/assets/eip-7943/contracts/uRWA1155.sol) token implementations is provided in assets folder. They use the OpenZeppelin library and include an account whitelist and enumerable role-based access control. These examples are provided for educational purposes only and are not audited. + + +​ +## [](#security-considerations) Security Considerations + +- **Access Control for `forcedTransfer` and `setFrozenTokens`:** The security of the mechanism chosen by the implementer to restrict access to these functions is paramount. Unauthorized access could lead to asset theft. Secure patterns (multisig, timelocks) are highly recommended. +- **Front-run of the `setFrozenTokens` function:** The `setFrozenTokens` function might be susceptible to front-running, similar to the `approve` function of the [ERC-20](https://eips.ethereum.org/EIPS/eip-20). If the suggestion of allowing freezing more than what an account owns is not followed, a front-run might be an incentive to an account to avoid any attempt of freezing its balance. Additional features to gradually increment or decrement the frozen status MAY be considered for implementation. +- **Implementation Logic:** The correctness of the *implementation* behind all interface functions is critical. Flaws in this logic could bypass intended transfer restrictions or incorrectly block valid transfers. +- **Standard Contract Security:** Implementations MUST adhere to general smart contract security best practices (reentrancy guards where applicable, checks-effects-interactions, etc.). Specifically in the checks-effects-interactions consideration, implementations need to be aware of tokens having hooks, especially on recipients like in [ERC-721](https://eips.ethereum.org/EIPS/eip-721) or [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155). In such circumstances it might be convenient to adopt reentrancy guards to prevent unwanted executions. diff --git a/doc/ERCSpecification/erc-8303-draft.md b/doc/ERCSpecification/erc-8303-draft.md new file mode 100644 index 0000000..15be396 --- /dev/null +++ b/doc/ERCSpecification/erc-8303-draft.md @@ -0,0 +1,131 @@ +--- +eip: 8303 +title: Contract Version +description: Interface for exposing a contract implementation version string +author: Ryan Sauge (@rya-sge) +discussions-to: https://ethereum-magicians.org/t/erc-8303-contract-version/28795 +status: Draft +type: Standards Track +category: ERC +created: 2026-02-12 +--- + +## Abstract + +This ERC defines a minimal interface to expose a contract version string through a standardized `version()` view function. The design is based on the version pattern used by [ERC-3643](./eip-3643.md), while remaining token-agnostic and applicable to other smart contract domains, including DeFi applications such as lending protocols. + +## Motivation + +Integrators frequently need a simple, on-chain way to identify which contract implementation they interact with. A standardized version function improves: + +- integration safety (feature gating by version), +- operations (faster incident triage), +- governance and migration tracking (upgrade visibility), +- ecosystem tooling interoperability. + +It is also useful for end-users, developers, and security auditors to identify which version of a codebase is currently used by a deployed contract. + +The same requirement appears in permissioned token systems ([ERC-3643](./eip-3643.md)) and in DeFi systems where contracts evolve over time. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). + +### Interface + +```solidity +interface IERC8303 { + /// @notice Returns the implementation version string. + /// @return The version value (for example "1.0.0"). + function version() external view returns (string memory); +} +``` + +### Required Behavior + +1. **Version read** + - `version()` MUST be a view function. + - `version()` MUST NOT revert under normal operation. + - `version()` MUST return a non-empty string. + +2. **Version meaning** + - Returned values SHOULD be stable and machine-comparable by off-chain tooling. + - Returned values SHOULD follow a Semantic Versioning 2.0.0-like format: `MAJOR.MINOR.PATCH` using decimal integers (for example `1.0.0`, `3.2.1`). + - The canonical recommended pattern is `^[0-9]+\.[0-9]+\.[0-9]+$`. + - Implementations MAY define their own versioning policy, but SHOULD document it publicly. + +3. **Deployment model compatibility** + - This interface is compatible with immutable deployments and proxy-based upgradeable deployments. + - In upgradeable systems, `version()` SHOULD reflect the active implementation seen by users and integrators. + +### [ERC-165](./eip-165.md) + +Implementations SHOULD support [ERC-165](./eip-165.md) interface discovery for this interface. + +If an implementation supports [ERC-165](./eip-165.md), `supportsInterface(type(IERC8303).interfaceId)` MUST return `true`. + +- The interface id for `IERC8303` is `0x54fd4d50`. + +### Compatibility Note for ERC-3643 Integrations + +Integrators MAY treat legacy ERC-3643 token contracts exposing a compatible `version()` function as implementing this ERC even if they do not advertise ERC-165 support. + +## Rationale + +- **Minimal scope**: A single function maximizes adoption and keeps gas/runtime complexity negligible. +- **ERC-3643 alignment**: Reuses a proven pattern already used in regulated token implementations. +- **Token-agnostic design**: The interface applies to token contracts and non-token contracts alike. +- **Optional ERC-165**: ERC-165 support is recommended but not required, lowering the adoption barrier for contracts that do not implement interface discovery. When ERC-165 is supported, advertising this interface is mandatory to ensure consistent detection by integrators. +- **`string` over `bytes32`**: A human-readable string is preferred to a fixed-size bytes32 for legibility in explorers and tooling, at the cost of marginally higher gas for the return value. + +## Backwards Compatibility + +This ERC is fully additive. Contracts already exposing `version()` are naturally compatible if they match the interface signature. + +## Test Cases + +The following test cases apply to any conforming implementation. + +1. `version()` MUST NOT revert. +2. `version()` MUST return a non-empty string. +3. `version()` MUST return the version string declared by the implementation (e.g. `"1.0.0"`). +4. If the contract supports [ERC-165](./eip-165.md), `supportsInterface(0x54fd4d50)` MUST return `true`. +5. If the contract supports [ERC-165](./eip-165.md), `supportsInterface(0xffffffff)` MUST return `false`. + +## Reference Implementation + +Reference implementations are provided in the assets folder: the [interface](../assets/erc-8303/src/IERC8303.sol) and a [base implementation](../assets/erc-8303/src/ERC8303.sol), along with usage examples for [ERC-20](../assets/erc-8303/src/examples/ERC20VersionedExample.sol) and [ERC-721](../assets/erc-8303/src/examples/ERC721VersionedExample.sol) tokens. These examples are provided for educational purposes only and are not audited. + +```solidity +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC8303.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +contract ERC8303Example is IERC8303, ERC165 { + function version() external pure override returns (string memory) { + return "1.0.0"; + } + + function supportsInterface(bytes4 interfaceId) + public + view + override + returns (bool) + { + return interfaceId == type(IERC8303).interfaceId + || super.supportsInterface(interfaceId); + } +} +``` + +## Security Considerations + +- `version()` is metadata and must not be used as a sole authorization primitive. +- In upgradeable systems, governance controls remain the trust anchor; version reporting does not prevent malicious upgrades. +- Integrators should combine version checks with other trust signals (governance model, audits, deployment provenance). + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/doc/FOUNDRY.md b/doc/FOUNDRY.md new file mode 100644 index 0000000..3cb9751 --- /dev/null +++ b/doc/FOUNDRY.md @@ -0,0 +1,214 @@ +# Foundry Toolchain Guide + +Full development-toolchain reference for the **Rules** project. For a quick command summary, see the [Toolchains and Usage](../README.md#toolchains-and-usage) section of the README. + +## Configuration + +Here are the settings for [Hardhat](https://hardhat.org) and [Foundry](https://getfoundry.sh). + +- `hardhat.config.js` + + - Solidity [v0.8.34](https://docs.soliditylang.org/en/v0.8.34/) + - EVM version: Prague (Pectra upgrade) + - Optimizer: true, 200 runs + +- `foundry.toml` + + - Solidity [v0.8.34](https://docs.soliditylang.org/en/v0.8.34/) + - EVM version: Prague (Pectra upgrade) + - Optimizer: true, 200 runs + +- Library + + - Foundry [v1.5.0](https://github.com/foundry-rs/foundry) + + - Forge std [v1.12.0](https://github.com/foundry-rs/forge-std/releases/tag/v1.12.0) + + - OpenZeppelin Contracts (submodule) [v5.6.1](https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v5.6.1) + + - OpenZeppelin Contracts Upgradeable (submodule) [v5.6.1](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/releases/tag/v5.6.1) + + - CMTAT [v3.3.0-rc1](https://github.com/CMTA/CMTAT/releases/tag/v3.3.0-rc1) + + - RuleEngine [v3.0.0-rc4](https://github.com/CMTA/RuleEngine/releases/tag/v3.0.0-rc4) + +## Toolchain installation + +This repository is primarily developed and tested with [Foundry](https://book.getfoundry.sh), a smart contract development toolchain. + +Hardhat configuration is also present to support contract compilation and a small smoke test with Hardhat. + +To install the Foundry suite, please refer to the official instructions in the [Foundry book](https://book.getfoundry.sh/getting-started/installation). + +## Initialization + +You must first initialize the submodules, with + +``` +forge install +``` + +See also the command's [documentation](https://book.getfoundry.sh/reference/forge/forge-install). + +Later you can update all the submodules with: + +``` +forge update +``` + +See also the command's [documentation](https://book.getfoundry.sh/reference/forge/forge-update). + +## Compilation + +The official documentation is available in the Foundry [website](https://book.getfoundry.sh/reference/forge/build-commands) + +``` + forge build +``` + +Hardhat compilation (optional): + +```bash +npm run hardhat:compile +``` + +## Contract size + +```bash + forge compile --sizes +``` + +## Testing + +You can run the tests with + +```bash +forge test +``` + +Hardhat smoke test (optional): + +```bash +npm run hardhat:test:smoke +``` + +To run a specific test, use + +```bash +forge test --match-contract --match-test +``` + +- For `RuleConditionalTransferLight` fuzz/integration tests, note that mint and burn paths (`from == address(0)` or `to == address(0)`) are intentionally exempt from approval consumption. +- `RuleMintAllowance` integration tests cover single mints, cumulative `batchMint` allowance consumption, rollback on over-allowance batch mint, and advertised ERC-165 interface IDs. +- Ownable2Step variants also include dedicated tests for ownership transfer and manager-only functions (IdentityRegistry, MaxTotalSupply, SanctionsList). +- Coverage-focused tests also target deployment wrappers and operation-rule overloads (`created`, `destroyed`, spender-aware `transferred`) to improve line/function coverage in `src/rules/operation` and `src/rules/validation/deployment`. + +Generate gas report + +```bash +forge test --gas-report +``` + +See also the test framework's [official documentation](https://book.getfoundry.sh/forge/tests), and that of the [test commands](https://book.getfoundry.sh/reference/forge/test-commands). + +## Gas Benchmarks + +Gas usage is tracked in two complementary files: + +- **`.gas-snapshot`** — machine-generated file produced by `forge snapshot`. It records the gas cost of every test function and is checked into the repository so that gas regressions are visible in diffs. Regenerate it with: + + ```bash + forge snapshot + ``` + + To check for regressions against the committed snapshot without overwriting it: + + ```bash + forge snapshot --check + ``` + +- **`doc/GAS.md`** — human-readable summary of key operation costs (e.g. `addAddress`, `detectTransferRestriction`) with the date of the last measurement. Update it manually after running `forge snapshot` when behaviour or gas costs change. + +## Coverage + +![coverage](./coverage/coverage.png) + +A code coverage is available in [index.html](./coverage/coverage/index.html). + +* Perform a code coverage + +``` +forge coverage +``` + +* Generate LCOV report + +``` +forge coverage --report lcov +``` + +- Generate `index.html` + +```bash +forge coverage --no-match-coverage "(script|mocks|test)" --report lcov && genhtml lcov.info --branch-coverage --prefix "$PWD/" --output-dir coverage +``` + +See [Solidity Coverage in VS Code with Foundry](https://mirror.xyz/devanon.eth/RrDvKPnlD-pmpuW7hQeR5wWdVjklrpOgPCOA-PJkWFU) & [Foundry forge coverage](https://www.rareskills.io/post/foundry-forge-coverage) + +## Other + +Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust. + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +### Documentation + +https://book.getfoundry.sh/ + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +> **Warning — private key security** +> Passing `--private-key` directly on the command line is **not recommended** in production: the key is visible in your shell history and to any process that can read `/proc`. Prefer hardware wallets (`--ledger`, `--trezor`), encrypted keystores (`--account `), or environment-variable signers. See [Foundry best practices](https://www.getfoundry.sh/best-practices) for details. + +```shell +$ forge script script/DeployCMTATWithWhitelist.s.sol --rpc-url --private-key +$ forge script script/DeployCMTATWithBlacklist.s.sol --rpc-url --private-key +$ forge script script/DeployCMTATWithBlacklistAndSanctionsList.s.sol --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/doc/coverage/coverage.png b/doc/coverage/coverage.png deleted file mode 100644 index 5535814..0000000 Binary files a/doc/coverage/coverage.png and /dev/null differ diff --git a/doc/coverage/coverage/index-sort-b.html b/doc/coverage/coverage/index-sort-b.html index 1109724..a435ddb 100644 --- a/doc/coverage/coverage/index-sort-b.html +++ b/doc/coverage/coverage/index-sort-b.html @@ -31,27 +31,27 @@ lcov.info Lines: - 764 - 781 + 1082 + 1106 97.8 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 270 - 287 - 94.1 % + 363 + 387 + 93.8 % Branches: - 161 - 161 - 100.0 % + 220 + 226 + 97.3 % @@ -82,14 +82,38 @@ Branches Sort by branch coverage - src/rules/operation + src/rules/validation/abstract/RuleERC2980
100.0%
100.0 % - 18 / 18 + 38 / 38 100.0 % - 6 / 6 + 12 / 12 + 83.3 % + 10 / 12 + + + src/rules/operation/abstract + +
98.4%98.4%
+ + 98.4 % + 240 / 244 + 94.8 % + 73 / 77 + 92.6 % + 50 / 54 + + + src/rules/operation + +
94.0%94.0%
+ + 94.0 % + 47 / 50 + 86.4 % + 19 / 22 - 0 / 0 @@ -99,9 +123,9 @@
100.0%
100.0 % - 133 / 133 + 164 / 164 100.0 % - 79 / 79 + 95 / 95 - 0 / 0 @@ -111,71 +135,47 @@
100.0%
100.0 % - 9 / 9 - 100.0 % - 3 / 3 + 13 / 13 100.0 % 4 / 4 - - - src/rules/validation/abstract/RuleERC2980 - -
100.0%
- 100.0 % - 36 / 36 - 100.0 % - 12 / 12 - 100.0 % - 8 / 8 + 4 / 4 src/rules/validation/abstract/RuleAddressSet -
96.3%96.3%
+
96.4%96.4%
- 96.3 % - 52 / 54 + 96.4 % + 54 / 56 90.5 % 19 / 21 100.0 % - 8 / 8 + 12 / 12 src/rules/validation/abstract/core -
93.3%93.3%
- - 93.3 % - 56 / 60 - 82.6 % - 19 / 23 - 100.0 % - 14 / 14 - - - src/rules/operation/abstract - -
97.4%97.4%
+
94.0%94.0%
- 97.4 % - 75 / 77 - 91.7 % - 22 / 24 + 94.0 % + 79 / 84 + 82.8 % + 24 / 29 100.0 % - 16 / 16 + 20 / 20 src/rules/validation/abstract/base -
97.7%97.7%
+
97.8%97.8%
- 97.7 % - 385 / 394 - 92.4 % - 110 / 119 + 97.8 % + 447 / 457 + 92.1 % + 117 / 127 100.0 % - 111 / 111 + 124 / 124 diff --git a/doc/coverage/coverage/index-sort-f.html b/doc/coverage/coverage/index-sort-f.html index a03f87b..320d953 100644 --- a/doc/coverage/coverage/index-sort-f.html +++ b/doc/coverage/coverage/index-sort-f.html @@ -31,27 +31,27 @@ lcov.info Lines: - 764 - 781 + 1082 + 1106 97.8 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 270 - 287 - 94.1 % + 363 + 387 + 93.8 % Branches: - 161 - 161 - 100.0 % + 220 + 226 + 97.3 % @@ -84,50 +84,62 @@ src/rules/validation/abstract/core -
93.3%93.3%
+
94.0%94.0%
- 93.3 % - 56 / 60 - 82.6 % - 19 / 23 + 94.0 % + 79 / 84 + 82.8 % + 24 / 29 100.0 % - 14 / 14 + 20 / 20 + + + src/rules/operation + +
94.0%94.0%
+ + 94.0 % + 47 / 50 + 86.4 % + 19 / 22 + - + 0 / 0 src/rules/validation/abstract/RuleAddressSet -
96.3%96.3%
+
96.4%96.4%
- 96.3 % - 52 / 54 + 96.4 % + 54 / 56 90.5 % 19 / 21 100.0 % - 8 / 8 + 12 / 12 - src/rules/operation/abstract + src/rules/validation/abstract/base -
97.4%97.4%
+
97.8%97.8%
- 97.4 % - 75 / 77 - 91.7 % - 22 / 24 + 97.8 % + 447 / 457 + 92.1 % + 117 / 127 100.0 % - 16 / 16 + 124 / 124 - src/rules/validation/abstract/base + src/rules/operation/abstract -
97.7%97.7%
+
98.4%98.4%
- 97.7 % - 385 / 394 - 92.4 % - 110 / 119 - 100.0 % - 111 / 111 + 98.4 % + 240 / 244 + 94.8 % + 73 / 77 + 92.6 % + 50 / 54 src/modules @@ -135,23 +147,11 @@
100.0%
100.0 % - 9 / 9 - 100.0 % - 3 / 3 + 13 / 13 100.0 % 4 / 4 - - - src/rules/operation - -
100.0%
- - 100.0 % - 18 / 18 100.0 % - 6 / 6 - - - 0 / 0 + 4 / 4 src/rules/validation/abstract/RuleERC2980 @@ -159,11 +159,11 @@
100.0%
100.0 % - 36 / 36 + 38 / 38 100.0 % 12 / 12 - 100.0 % - 8 / 8 + 83.3 % + 10 / 12 src/rules/validation/deployment @@ -171,9 +171,9 @@
100.0%
100.0 % - 133 / 133 + 164 / 164 100.0 % - 79 / 79 + 95 / 95 - 0 / 0 diff --git a/doc/coverage/coverage/index-sort-l.html b/doc/coverage/coverage/index-sort-l.html index 7f305a4..198f184 100644 --- a/doc/coverage/coverage/index-sort-l.html +++ b/doc/coverage/coverage/index-sort-l.html @@ -31,27 +31,27 @@ lcov.info Lines: - 764 - 781 + 1082 + 1106 97.8 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 270 - 287 - 94.1 % + 363 + 387 + 93.8 % Branches: - 161 - 161 - 100.0 % + 220 + 226 + 97.3 % @@ -81,53 +81,65 @@ Functions Sort by function coverage Branches Sort by branch coverage + + src/rules/operation + +
94.0%94.0%
+ + 94.0 % + 47 / 50 + 86.4 % + 19 / 22 + - + 0 / 0 + src/rules/validation/abstract/core -
93.3%93.3%
+
94.0%94.0%
- 93.3 % - 56 / 60 - 82.6 % - 19 / 23 + 94.0 % + 79 / 84 + 82.8 % + 24 / 29 100.0 % - 14 / 14 + 20 / 20 src/rules/validation/abstract/RuleAddressSet -
96.3%96.3%
+
96.4%96.4%
- 96.3 % - 52 / 54 + 96.4 % + 54 / 56 90.5 % 19 / 21 100.0 % - 8 / 8 + 12 / 12 - src/rules/operation/abstract + src/rules/validation/abstract/base -
97.4%97.4%
+
97.8%97.8%
- 97.4 % - 75 / 77 - 91.7 % - 22 / 24 + 97.8 % + 447 / 457 + 92.1 % + 117 / 127 100.0 % - 16 / 16 + 124 / 124 - src/rules/validation/abstract/base + src/rules/operation/abstract -
97.7%97.7%
+
98.4%98.4%
- 97.7 % - 385 / 394 - 92.4 % - 110 / 119 - 100.0 % - 111 / 111 + 98.4 % + 240 / 244 + 94.8 % + 73 / 77 + 92.6 % + 50 / 54 src/modules @@ -135,23 +147,11 @@
100.0%
100.0 % - 9 / 9 - 100.0 % - 3 / 3 + 13 / 13 100.0 % 4 / 4 - - - src/rules/operation - -
100.0%
- - 100.0 % - 18 / 18 100.0 % - 6 / 6 - - - 0 / 0 + 4 / 4 src/rules/validation/abstract/RuleERC2980 @@ -159,11 +159,11 @@
100.0%
100.0 % - 36 / 36 + 38 / 38 100.0 % 12 / 12 - 100.0 % - 8 / 8 + 83.3 % + 10 / 12 src/rules/validation/deployment @@ -171,9 +171,9 @@
100.0%
100.0 % - 133 / 133 + 164 / 164 100.0 % - 79 / 79 + 95 / 95 - 0 / 0 diff --git a/doc/coverage/coverage/index.html b/doc/coverage/coverage/index.html index 0394a54..0065a38 100644 --- a/doc/coverage/coverage/index.html +++ b/doc/coverage/coverage/index.html @@ -31,27 +31,27 @@ lcov.info Lines: - 764 - 781 + 1082 + 1106 97.8 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 270 - 287 - 94.1 % + 363 + 387 + 93.8 % Branches: - 161 - 161 - 100.0 % + 220 + 226 + 97.3 % @@ -87,47 +87,47 @@
100.0%
100.0 % - 9 / 9 + 13 / 13 100.0 % - 3 / 3 + 4 / 4 100.0 % 4 / 4 src/rules/operation -
100.0%
+
94.0%94.0%
- 100.0 % - 18 / 18 - 100.0 % - 6 / 6 + 94.0 % + 47 / 50 + 86.4 % + 19 / 22 - 0 / 0 src/rules/operation/abstract -
97.4%97.4%
+
98.4%98.4%
- 97.4 % - 75 / 77 - 91.7 % - 22 / 24 - 100.0 % - 16 / 16 + 98.4 % + 240 / 244 + 94.8 % + 73 / 77 + 92.6 % + 50 / 54 src/rules/validation/abstract/RuleAddressSet -
96.3%96.3%
+
96.4%96.4%
- 96.3 % - 52 / 54 + 96.4 % + 54 / 56 90.5 % 19 / 21 100.0 % - 8 / 8 + 12 / 12 src/rules/validation/abstract/RuleERC2980 @@ -135,35 +135,35 @@
100.0%
100.0 % - 36 / 36 + 38 / 38 100.0 % 12 / 12 - 100.0 % - 8 / 8 + 83.3 % + 10 / 12 src/rules/validation/abstract/base -
97.7%97.7%
+
97.8%97.8%
- 97.7 % - 385 / 394 - 92.4 % - 110 / 119 + 97.8 % + 447 / 457 + 92.1 % + 117 / 127 100.0 % - 111 / 111 + 124 / 124 src/rules/validation/abstract/core -
93.3%93.3%
+
94.0%94.0%
- 93.3 % - 56 / 60 - 82.6 % - 19 / 23 + 94.0 % + 79 / 84 + 82.8 % + 24 / 29 100.0 % - 14 / 14 + 20 / 20 src/rules/validation/deployment @@ -171,9 +171,9 @@
100.0%
100.0 % - 133 / 133 + 164 / 164 100.0 % - 79 / 79 + 95 / 95 - 0 / 0 diff --git a/doc/coverage/coverage/src/modules/AccessControlModuleStandalone.sol.func-sort-c.html b/doc/coverage/coverage/src/modules/AccessControlModuleStandalone.sol.func-sort-c.html index d4a9571..cdddaa4 100644 --- a/doc/coverage/coverage/src/modules/AccessControlModuleStandalone.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/modules/AccessControlModuleStandalone.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 2 @@ -69,12 +69,12 @@ Hit count Sort by hit count - AccessControlModuleStandalone.constructor - 430 + AccessControlModuleStandalone.constructor + 1622 - AccessControlModuleStandalone.hasRole - 540 + AccessControlModuleStandalone.hasRole + 1746
diff --git a/doc/coverage/coverage/src/modules/AccessControlModuleStandalone.sol.func.html b/doc/coverage/coverage/src/modules/AccessControlModuleStandalone.sol.func.html index 18223ea..3394006 100644 --- a/doc/coverage/coverage/src/modules/AccessControlModuleStandalone.sol.func.html +++ b/doc/coverage/coverage/src/modules/AccessControlModuleStandalone.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 2 @@ -69,12 +69,12 @@ Hit count Sort by hit count - AccessControlModuleStandalone.constructor - 430 + AccessControlModuleStandalone.constructor + 1622 - AccessControlModuleStandalone.hasRole - 540 + AccessControlModuleStandalone.hasRole + 1746
diff --git a/doc/coverage/coverage/src/modules/AccessControlModuleStandalone.sol.gcov.html b/doc/coverage/coverage/src/modules/AccessControlModuleStandalone.sol.gcov.html index 494c0e8..fda071a 100644 --- a/doc/coverage/coverage/src/modules/AccessControlModuleStandalone.sol.gcov.html +++ b/doc/coverage/coverage/src/modules/AccessControlModuleStandalone.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 2 @@ -78,54 +78,58 @@ 7 : : import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; 8 : : import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; 9 : : - 10 : : abstract contract AccessControlModuleStandalone is AccessControlEnumerable { - 11 : : error AccessControlModuleStandalone_AddressZeroNotAllowed(); - 12 : : - 13 : : /*////////////////////////////////////////////////////////////// - 14 : : CONSTRUCTOR - 15 : : //////////////////////////////////////////////////////////////*/ - 16 : : - 17 : : /** - 18 : : * @notice Assigns the provided address as the default admin. - 19 : : * @dev - 20 : : * - Reverts if `admin` is the zero address. - 21 : : * - Grants `DEFAULT_ADMIN_ROLE` to `admin`. - 22 : : * The return value of `_grantRole` is intentionally ignored, as it returns `false` - 23 : : * only when the role was already granted. - 24 : : * - 25 : : * @param admin The address that will receive the `DEFAULT_ADMIN_ROLE`. - 26 : : */ - 27 : 430 : constructor(address admin) { - 28 [ + + ]: 430 : require(admin != address(0), AccessControlModuleStandalone_AddressZeroNotAllowed()); - 29 : : // we don't check the return value - 30 : : // _grantRole attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted. - 31 : : // return false only if the admin has already the role - 32 : 424 : _grantRole(DEFAULT_ADMIN_ROLE, admin); - 33 : : } - 34 : : - 35 : : /*////////////////////////////////////////////////////////////// - 36 : : PUBLIC FUNCTIONS - 37 : : //////////////////////////////////////////////////////////////*/ - 38 : : - 39 : : /** - 40 : : * @dev Returns `true` if `account` has been granted `role`. - 41 : : */ - 42 : 540 : function hasRole(bytes32 role, address account) - 43 : : public - 44 : : view - 45 : : virtual - 46 : : override(AccessControl, IAccessControl) - 47 : : returns (bool) - 48 : : { - 49 : : // Dev note: default admin is treated as having all roles but may not appear in enumerable role members. - 50 : : // The Default Admin has all roles - 51 [ + + ]: 3627 : if (AccessControl.hasRole(DEFAULT_ADMIN_ROLE, account)) { - 52 : 3104 : return true; - 53 : : } else { - 54 : 523 : return AccessControl.hasRole(role, account); - 55 : : } - 56 : : } - 57 : : } + 10 : : /** + 11 : : * @title AccessControlModuleStandalone — base RBAC module where the default admin implicitly holds all roles. + 12 : : */ + 13 : : abstract contract AccessControlModuleStandalone is AccessControlEnumerable { + 14 : : error AccessControlModuleStandalone_AddressZeroNotAllowed(); + 15 : : + 16 : : /*////////////////////////////////////////////////////////////// + 17 : : CONSTRUCTOR + 18 : : //////////////////////////////////////////////////////////////*/ + 19 : : + 20 : : /** + 21 : : * @notice Assigns the provided address as the default admin. + 22 : : * @dev + 23 : : * - Reverts if `admin` is the zero address. + 24 : : * - Grants `DEFAULT_ADMIN_ROLE` to `admin`. + 25 : : * The return value of `_grantRole` is intentionally ignored, as it returns `false` + 26 : : * only when the role was already granted. + 27 : : * + 28 : : * @param admin The address that will receive the `DEFAULT_ADMIN_ROLE`. + 29 : : */ + 30 : 1622 : constructor(address admin) { + 31 [ + + ]: 1622 : require(admin != address(0), AccessControlModuleStandalone_AddressZeroNotAllowed()); + 32 : : // we don't check the return value + 33 : : // _grantRole attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted. + 34 : : // return false only if the admin has already the role + 35 : 1615 : _grantRole(DEFAULT_ADMIN_ROLE, admin); + 36 : : } + 37 : : + 38 : : /*////////////////////////////////////////////////////////////// + 39 : : PUBLIC FUNCTIONS + 40 : : //////////////////////////////////////////////////////////////*/ + 41 : : + 42 : : /** + 43 : : * @inheritdoc IAccessControl + 44 : : * @dev The default admin is treated as holding every role. + 45 : : */ + 46 : 1746 : function hasRole(bytes32 role, address account) + 47 : : public + 48 : : view + 49 : : virtual + 50 : : override(AccessControl, IAccessControl) + 51 : : returns (bool) + 52 : : { + 53 : : // Dev note: default admin is treated as having all roles but may not appear in enumerable role members. + 54 : : // The Default Admin has all roles + 55 [ + + ]: 21064 : if (AccessControl.hasRole(DEFAULT_ADMIN_ROLE, account)) { + 56 : 4032 : return true; + 57 : : } else { + 58 : 17032 : return AccessControl.hasRole(role, account); + 59 : : } + 60 : : } + 61 : : } diff --git a/doc/coverage/coverage/src/modules/Ownable2StepERC165Module.sol.func-sort-c.html b/doc/coverage/coverage/src/modules/Ownable2StepERC165Module.sol.func-sort-c.html new file mode 100644 index 0000000..8bcb20a --- /dev/null +++ b/doc/coverage/coverage/src/modules/Ownable2StepERC165Module.sol.func-sort-c.html @@ -0,0 +1,85 @@ + + + + + + + LCOV - lcov.info - src/modules/Ownable2StepERC165Module.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - Ownable2StepERC165Module.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:44100.0 %
Date:2026-07-14 13:44:06Functions:11100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
Ownable2StepERC165Module.supportsInterface68
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/modules/Ownable2StepERC165Module.sol.func.html b/doc/coverage/coverage/src/modules/Ownable2StepERC165Module.sol.func.html new file mode 100644 index 0000000..c697bd8 --- /dev/null +++ b/doc/coverage/coverage/src/modules/Ownable2StepERC165Module.sol.func.html @@ -0,0 +1,85 @@ + + + + + + + LCOV - lcov.info - src/modules/Ownable2StepERC165Module.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - Ownable2StepERC165Module.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:44100.0 %
Date:2026-07-14 13:44:06Functions:11100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
Ownable2StepERC165Module.supportsInterface68
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/modules/Ownable2StepERC165Module.sol.gcov.html b/doc/coverage/coverage/src/modules/Ownable2StepERC165Module.sol.gcov.html new file mode 100644 index 0000000..a3d9ebb --- /dev/null +++ b/doc/coverage/coverage/src/modules/Ownable2StepERC165Module.sol.gcov.html @@ -0,0 +1,107 @@ + + + + + + + LCOV - lcov.info - src/modules/Ownable2StepERC165Module.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/modules - Ownable2StepERC165Module.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:44100.0 %
Date:2026-07-14 13:44:06Functions:11100.0 %
Branches:00-
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : pragma solidity ^0.8.20;
+       3                 :            : 
+       4                 :            : import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
+       5                 :            : import {OwnableInterfaceId} from "RuleEngine/modules/library/OwnableInterfaceId.sol";
+       6                 :            : import {Ownable2StepInterfaceId} from "RuleEngine/modules/library/Ownable2StepInterfaceId.sol";
+       7                 :            : 
+       8                 :            : /**
+       9                 :            :  * @title Ownable2StepERC165Module
+      10                 :            :  * @notice Shared ERC-165 advertisement for Ownable2Step deployments.
+      11                 :            :  */
+      12                 :            : abstract contract Ownable2StepERC165Module is ERC165 {
+      13                 :            :     /**
+      14                 :            :      * @inheritdoc ERC165
+      15                 :            :      * @dev Also advertises support for the IERC173 and IOwnable2Step interfaces.
+      16                 :            :      */
+      17                 :         68 :     function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
+      18                 :         68 :         return interfaceId == OwnableInterfaceId.IERC173_INTERFACE_ID
+      19                 :         57 :             || interfaceId == Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID
+      20                 :         46 :             || ERC165.supportsInterface(interfaceId);
+      21                 :            :     }
+      22                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/modules/VersionModule.sol.func-sort-c.html b/doc/coverage/coverage/src/modules/VersionModule.sol.func-sort-c.html index 5154c91..2f6463d 100644 --- a/doc/coverage/coverage/src/modules/VersionModule.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/modules/VersionModule.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 1 @@ -69,7 +69,7 @@ Hit count Sort by hit count - VersionModule.version + VersionModule.version 7 diff --git a/doc/coverage/coverage/src/modules/VersionModule.sol.func.html b/doc/coverage/coverage/src/modules/VersionModule.sol.func.html index df259b7..f24e5ae 100644 --- a/doc/coverage/coverage/src/modules/VersionModule.sol.func.html +++ b/doc/coverage/coverage/src/modules/VersionModule.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 1 @@ -69,7 +69,7 @@ Hit count Sort by hit count - VersionModule.version + VersionModule.version 7 diff --git a/doc/coverage/coverage/src/modules/VersionModule.sol.gcov.html b/doc/coverage/coverage/src/modules/VersionModule.sol.gcov.html index 10bbfc1..c65187e 100644 --- a/doc/coverage/coverage/src/modules/VersionModule.sol.gcov.html +++ b/doc/coverage/coverage/src/modules/VersionModule.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 1 @@ -79,17 +79,22 @@ 8 : : * @notice Exposes the contract version as required by ERC-3643. 9 : : */ 10 : : abstract contract VersionModule is IERC3643Version { - 11 : : string private constant VERSION = "0.3.0"; - 12 : : - 13 : : /*////////////////////////////////////////////////////////////// - 14 : : PUBLIC FUNCTIONS - 15 : : //////////////////////////////////////////////////////////////*/ - 16 : : - 17 : : /// @inheritdoc IERC3643Version - 18 : 7 : function version() public view virtual override returns (string memory version_) { - 19 : 7 : return VERSION; - 20 : : } - 21 : : } + 11 : : /** + 12 : : * @notice The contract version string returned by {version}. + 13 : : */ + 14 : : string private constant VERSION = "0.4.0"; + 15 : : + 16 : : /*////////////////////////////////////////////////////////////// + 17 : : PUBLIC FUNCTIONS + 18 : : //////////////////////////////////////////////////////////////*/ + 19 : : + 20 : : /** + 21 : : * @inheritdoc IERC3643Version + 22 : : */ + 23 : 7 : function version() public view virtual override returns (string memory version_) { + 24 : 7 : return VERSION; + 25 : : } + 26 : : } diff --git a/doc/coverage/coverage/src/modules/index-sort-b.html b/doc/coverage/coverage/src/modules/index-sort-b.html index b5c8072..d13f274 100644 --- a/doc/coverage/coverage/src/modules/index-sort-b.html +++ b/doc/coverage/coverage/src/modules/index-sort-b.html @@ -31,17 +31,17 @@ lcov.info Lines: - 9 - 9 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 3 - 3 + 4 + 4 100.0 % @@ -81,6 +81,18 @@ Functions Sort by function coverage Branches Sort by branch coverage + + Ownable2StepERC165Module.sol + +
100.0%
+ + 100.0 % + 4 / 4 + 100.0 % + 1 / 1 + - + 0 / 0 + VersionModule.sol diff --git a/doc/coverage/coverage/src/modules/index-sort-f.html b/doc/coverage/coverage/src/modules/index-sort-f.html index 73f9fb0..820d302 100644 --- a/doc/coverage/coverage/src/modules/index-sort-f.html +++ b/doc/coverage/coverage/src/modules/index-sort-f.html @@ -31,17 +31,17 @@ lcov.info Lines: - 9 - 9 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 3 - 3 + 4 + 4 100.0 % @@ -81,6 +81,18 @@ Functions Sort by function coverage Branches Sort by branch coverage + + Ownable2StepERC165Module.sol + +
100.0%
+ + 100.0 % + 4 / 4 + 100.0 % + 1 / 1 + - + 0 / 0 + VersionModule.sol diff --git a/doc/coverage/coverage/src/modules/index-sort-l.html b/doc/coverage/coverage/src/modules/index-sort-l.html index 9a0c19c..4f4dca0 100644 --- a/doc/coverage/coverage/src/modules/index-sort-l.html +++ b/doc/coverage/coverage/src/modules/index-sort-l.html @@ -31,17 +31,17 @@ lcov.info Lines: - 9 - 9 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 3 - 3 + 4 + 4 100.0 % @@ -93,6 +93,18 @@ - 0 / 0 + + Ownable2StepERC165Module.sol + +
100.0%
+ + 100.0 % + 4 / 4 + 100.0 % + 1 / 1 + - + 0 / 0 + AccessControlModuleStandalone.sol diff --git a/doc/coverage/coverage/src/modules/index.html b/doc/coverage/coverage/src/modules/index.html index 73f8778..1707140 100644 --- a/doc/coverage/coverage/src/modules/index.html +++ b/doc/coverage/coverage/src/modules/index.html @@ -31,17 +31,17 @@ lcov.info Lines: - 9 - 9 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 3 - 3 + 4 + 4 100.0 % @@ -93,6 +93,18 @@ 100.0 % 4 / 4 + + Ownable2StepERC165Module.sol + +
100.0%
+ + 100.0 % + 4 / 4 + 100.0 % + 1 / 1 + - + 0 / 0 + VersionModule.sol diff --git a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLight.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLight.sol.func-sort-c.html index a847612..7356557 100644 --- a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLight.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLight.sol.func-sort-c.html @@ -37,11 +37,11 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 3 - 3 + 4 + 4 100.0 % @@ -69,16 +69,20 @@ Hit count Sort by hit count - RuleConditionalTransferLight.supportsInterface - 21 + RuleConditionalTransferLight._authorizeComplianceBindingChange + 3 - RuleConditionalTransferLight._onlyComplianceManager - 25 + RuleConditionalTransferLight.supportsInterface + 48 - RuleConditionalTransferLight._authorizeTransferApproval - 1924 + RuleConditionalTransferLight._onlyComplianceManager + 58 + + + RuleConditionalTransferLight._authorizeTransferApproval + 7607
diff --git a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLight.sol.func.html b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLight.sol.func.html index c28dd62..0677d52 100644 --- a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLight.sol.func.html +++ b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLight.sol.func.html @@ -37,11 +37,11 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 3 - 3 + 4 + 4 100.0 % @@ -69,16 +69,20 @@ Hit count Sort by hit count - RuleConditionalTransferLight._authorizeTransferApproval - 1924 + RuleConditionalTransferLight._authorizeComplianceBindingChange + 3 - RuleConditionalTransferLight._onlyComplianceManager - 25 + RuleConditionalTransferLight._authorizeTransferApproval + 7607 - RuleConditionalTransferLight.supportsInterface - 21 + RuleConditionalTransferLight._onlyComplianceManager + 58 + + + RuleConditionalTransferLight.supportsInterface + 48
diff --git a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLight.sol.gcov.html b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLight.sol.gcov.html index 34c83af..56926ff 100644 --- a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLight.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLight.sol.gcov.html @@ -37,11 +37,11 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 3 - 3 + 4 + 4 100.0 % @@ -81,49 +81,73 @@ 10 : : import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol"; 11 : : import {AccessControlModuleStandalone} from "../../modules/AccessControlModuleStandalone.sol"; 12 : : import {RuleConditionalTransferLightBase} from "./abstract/RuleConditionalTransferLightBase.sol"; - 13 : : - 14 : : /** - 15 : : * @title ConditionalTransferLight - 16 : : * @dev Requires operator approval for each transfer. Same transfer (from, to, value) - 17 : : * can be approved multiple times to allow repeated transfers. - 18 : : */ - 19 : : contract RuleConditionalTransferLight is AccessControlModuleStandalone, RuleConditionalTransferLightBase { - 20 : : /*////////////////////////////////////////////////////////////// - 21 : : CONSTRUCTOR - 22 : : //////////////////////////////////////////////////////////////*/ - 23 : : - 24 : : /** - 25 : : * @param admin Address of the contract admin. - 26 : : */ - 27 : : constructor(address admin) AccessControlModuleStandalone(admin) {} + 13 : : import {ERC3643ComplianceRolesStorage} from "RuleEngine/modules/library/ERC3643ComplianceRolesStorage.sol"; + 14 : : + 15 : : /** + 16 : : * @title ConditionalTransferLight + 17 : : * @dev Requires operator approval for each transfer. Same transfer (from, to, value) + 18 : : * can be approved multiple times to allow repeated transfers. + 19 : : */ + 20 : : contract RuleConditionalTransferLight is + 21 : : AccessControlModuleStandalone, + 22 : : RuleConditionalTransferLightBase, + 23 : : ERC3643ComplianceRolesStorage + 24 : : { + 25 : : /*////////////////////////////////////////////////////////////// + 26 : : CONSTRUCTOR + 27 : : //////////////////////////////////////////////////////////////*/ 28 : : - 29 : : /*////////////////////////////////////////////////////////////// - 30 : : PUBLIC FUNCTIONS - 31 : : //////////////////////////////////////////////////////////////*/ - 32 : : - 33 : 21 : function supportsInterface(bytes4 interfaceId) - 34 : : public - 35 : : view - 36 : : virtual - 37 : : override(AccessControlEnumerable, IERC165) - 38 : : returns (bool) - 39 : : { - 40 : 21 : return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID - 41 : 20 : || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID - 42 : 19 : || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID - 43 : 13 : || interfaceId == type(IERC7551Compliance).interfaceId - 44 : 12 : || interfaceId == type(IERC3643ComplianceFull).interfaceId - 45 : 11 : || AccessControlEnumerable.supportsInterface(interfaceId); - 46 : : } - 47 : : - 48 : : /*////////////////////////////////////////////////////////////// - 49 : : ACCESS CONTROL - 50 : : //////////////////////////////////////////////////////////////*/ - 51 : : - 52 : 1924 : function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} - 53 : : - 54 : 25 : function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} - 55 : : } + 29 : : /** + 30 : : * @param admin Address of the contract admin. + 31 : : */ + 32 : : constructor(address admin) AccessControlModuleStandalone(admin) {} + 33 : : + 34 : : /*////////////////////////////////////////////////////////////// + 35 : : PUBLIC FUNCTIONS + 36 : : //////////////////////////////////////////////////////////////*/ + 37 : : + 38 : : /** + 39 : : * @inheritdoc IERC165 + 40 : : */ + 41 : 48 : function supportsInterface(bytes4 interfaceId) + 42 : : public + 43 : : view + 44 : : virtual + 45 : : override(AccessControlEnumerable, IERC165) + 46 : : returns (bool) + 47 : : { + 48 : 48 : return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + 49 : 47 : || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + 50 : 46 : || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId + 51 : 30 : || interfaceId == type(IERC3643ComplianceFull).interfaceId + 52 : 29 : || AccessControlEnumerable.supportsInterface(interfaceId); + 53 : : } + 54 : : + 55 : : /*////////////////////////////////////////////////////////////// + 56 : : ACCESS CONTROL + 57 : : //////////////////////////////////////////////////////////////*/ + 58 : : + 59 : : /** + 60 : : * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`. + 61 : : */ + 62 : 58 : function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + 63 : : + 64 : : /** + 65 : : * @notice Reverts unless the caller holds `OPERATOR_ROLE`. + 66 : : */ + 67 : 7607 : function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} + 68 : : + 69 : : /** + 70 : : * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`. + 71 : : */ + 72 : 3 : function _authorizeComplianceBindingChange(address) + 73 : : internal + 74 : : view + 75 : : virtual + 76 : : override + 77 : : onlyRole(COMPLIANCE_MANAGER_ROLE) + 78 : : {} + 79 : : } diff --git a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiToken.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiToken.sol.func-sort-c.html new file mode 100644 index 0000000..ba65d0e --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiToken.sol.func-sort-c.html @@ -0,0 +1,93 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/RuleConditionalTransferLightMultiToken.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation - RuleConditionalTransferLightMultiToken.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:88100.0 %
Date:2026-07-14 13:44:06Functions:33100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleConditionalTransferLightMultiToken.supportsInterface6
RuleConditionalTransferLightMultiToken._authorizeTransferApproval31
RuleConditionalTransferLightMultiToken._onlyComplianceManager38
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiToken.sol.func.html b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiToken.sol.func.html new file mode 100644 index 0000000..b92fbd5 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiToken.sol.func.html @@ -0,0 +1,93 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/RuleConditionalTransferLightMultiToken.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation - RuleConditionalTransferLightMultiToken.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:88100.0 %
Date:2026-07-14 13:44:06Functions:33100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleConditionalTransferLightMultiToken._authorizeTransferApproval31
RuleConditionalTransferLightMultiToken._onlyComplianceManager38
RuleConditionalTransferLightMultiToken.supportsInterface6
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiToken.sol.gcov.html b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiToken.sol.gcov.html new file mode 100644 index 0000000..4475a27 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiToken.sol.gcov.html @@ -0,0 +1,141 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/RuleConditionalTransferLightMultiToken.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation - RuleConditionalTransferLightMultiToken.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:88100.0 %
Date:2026-07-14 13:44:06Functions:33100.0 %
Branches:00-
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : pragma solidity ^0.8.20;
+       3                 :            : 
+       4                 :            : import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol";
+       5                 :            : import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+       6                 :            : import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol";
+       7                 :            : import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol";
+       8                 :            : import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol";
+       9                 :            : import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
+      10                 :            : import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol";
+      11                 :            : import {AccessControlModuleStandalone} from "../../modules/AccessControlModuleStandalone.sol";
+      12                 :            : import {RuleConditionalTransferLightMultiTokenBase} from "./abstract/RuleConditionalTransferLightMultiTokenBase.sol";
+      13                 :            : import {ERC3643ComplianceRolesStorage} from "RuleEngine/modules/library/ERC3643ComplianceRolesStorage.sol";
+      14                 :            : 
+      15                 :            : /**
+      16                 :            :  * @title RuleConditionalTransferLightMultiToken
+      17                 :            :  * @notice AccessControl variant of the multi-token conditional transfer rule.
+      18                 :            :  *         `OPERATOR_ROLE` approves transfers; `COMPLIANCE_MANAGER_ROLE` manages compliance bindings.
+      19                 :            :  */
+      20                 :            : contract RuleConditionalTransferLightMultiToken is
+      21                 :            :     AccessControlModuleStandalone,
+      22                 :            :     RuleConditionalTransferLightMultiTokenBase,
+      23                 :            :     ERC3643ComplianceRolesStorage
+      24                 :            : {
+      25                 :            :     /**
+      26                 :            :      * @param admin Address of the contract admin.
+      27                 :            :      */
+      28                 :            :     constructor(address admin) AccessControlModuleStandalone(admin) {}
+      29                 :            : 
+      30                 :            :     /**
+      31                 :            :      * @inheritdoc IERC165
+      32                 :            :      */
+      33                 :          6 :     function supportsInterface(bytes4 interfaceId)
+      34                 :            :         public
+      35                 :            :         view
+      36                 :            :         virtual
+      37                 :            :         override(AccessControlEnumerable, IERC165)
+      38                 :            :         returns (bool)
+      39                 :            :     {
+      40                 :          6 :         return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID
+      41                 :          6 :             || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID
+      42                 :          6 :             || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId
+      43                 :          4 :             || interfaceId == type(IERC3643ComplianceFull).interfaceId
+      44                 :          4 :             || AccessControlEnumerable.supportsInterface(interfaceId);
+      45                 :            :     }
+      46                 :            : 
+      47                 :            :     /**
+      48                 :            :      * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`.
+      49                 :            :      */
+      50                 :         38 :     function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+      51                 :            : 
+      52                 :            :     /**
+      53                 :            :      * @notice Reverts unless the caller holds `OPERATOR_ROLE`.
+      54                 :            :      */
+      55                 :         31 :     function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {}
+      56                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol.func-sort-c.html new file mode 100644 index 0000000..0dba0d3 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol.func-sort-c.html @@ -0,0 +1,93 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation - RuleConditionalTransferLightMultiTokenOwnable2Step.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:6875.0 %
Date:2026-07-14 13:44:06Functions:1333.3 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleConditionalTransferLightMultiTokenOwnable2Step._authorizeTransferApproval0
RuleConditionalTransferLightMultiTokenOwnable2Step._onlyComplianceManager0
RuleConditionalTransferLightMultiTokenOwnable2Step.supportsInterface5
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol.func.html b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol.func.html new file mode 100644 index 0000000..7bddc55 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol.func.html @@ -0,0 +1,93 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation - RuleConditionalTransferLightMultiTokenOwnable2Step.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:6875.0 %
Date:2026-07-14 13:44:06Functions:1333.3 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleConditionalTransferLightMultiTokenOwnable2Step._authorizeTransferApproval0
RuleConditionalTransferLightMultiTokenOwnable2Step._onlyComplianceManager0
RuleConditionalTransferLightMultiTokenOwnable2Step.supportsInterface5
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol.gcov.html b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol.gcov.html new file mode 100644 index 0000000..4811963 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol.gcov.html @@ -0,0 +1,140 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation - RuleConditionalTransferLightMultiTokenOwnable2Step.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:6875.0 %
Date:2026-07-14 13:44:06Functions:1333.3 %
Branches:00-
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : pragma solidity ^0.8.20;
+       3                 :            : 
+       4                 :            : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
+       5                 :            : import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
+       6                 :            : import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+       7                 :            : import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol";
+       8                 :            : import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol";
+       9                 :            : import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol";
+      10                 :            : import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
+      11                 :            : import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol";
+      12                 :            : import {RuleConditionalTransferLightMultiTokenBase} from "./abstract/RuleConditionalTransferLightMultiTokenBase.sol";
+      13                 :            : import {Ownable2StepERC165Module} from "../../modules/Ownable2StepERC165Module.sol";
+      14                 :            : 
+      15                 :            : /**
+      16                 :            :  * @title RuleConditionalTransferLightMultiTokenOwnable2Step
+      17                 :            :  * @notice Ownable2Step variant of the multi-token conditional transfer rule.
+      18                 :            :  *         The owner approves transfers and manages compliance bindings.
+      19                 :            :  */
+      20                 :            : contract RuleConditionalTransferLightMultiTokenOwnable2Step is
+      21                 :            :     RuleConditionalTransferLightMultiTokenBase,
+      22                 :            :     Ownable2Step,
+      23                 :            :     Ownable2StepERC165Module
+      24                 :            : {
+      25                 :            :     /**
+      26                 :            :      * @param owner Address of the contract owner.
+      27                 :            :      */
+      28                 :            :     constructor(address owner) Ownable(owner) {}
+      29                 :            : 
+      30                 :            :     /**
+      31                 :            :      * @inheritdoc IERC165
+      32                 :            :      */
+      33                 :          5 :     function supportsInterface(bytes4 interfaceId)
+      34                 :            :         public
+      35                 :            :         view
+      36                 :            :         override(Ownable2StepERC165Module, IERC165)
+      37                 :            :         returns (bool)
+      38                 :            :     {
+      39                 :          5 :         return Ownable2StepERC165Module.supportsInterface(interfaceId)
+      40                 :          2 :             || interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID
+      41                 :          2 :             || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID
+      42                 :          2 :             || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId
+      43                 :          2 :             || interfaceId == type(IERC3643ComplianceFull).interfaceId;
+      44                 :            :     }
+      45                 :            : 
+      46                 :            :     /**
+      47                 :            :      * @notice Reverts unless the caller is the owner.
+      48                 :            :      */
+      49                 :          0 :     function _onlyComplianceManager() internal view virtual override onlyOwner {}
+      50                 :            : 
+      51                 :            :     /**
+      52                 :            :      * @notice Reverts unless the caller is the owner.
+      53                 :            :      */
+      54                 :          0 :     function _authorizeTransferApproval() internal view virtual override onlyOwner {}
+      55                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol.func-sort-c.html index ae97ffa..9538430 100644 --- a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol.func-sort-c.html @@ -31,18 +31,18 @@ lcov.info Lines: + 8 9 - 9 - 100.0 % + 88.9 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 3 - 3 - 100.0 % + 4 + 75.0 % @@ -69,16 +69,20 @@ Hit count Sort by hit count - RuleConditionalTransferLightOwnable2Step._onlyComplianceManager + RuleConditionalTransferLightOwnable2Step._authorizeComplianceBindingChange + 0 + + + RuleConditionalTransferLightOwnable2Step._onlyComplianceManager 3 - RuleConditionalTransferLightOwnable2Step._authorizeTransferApproval + RuleConditionalTransferLightOwnable2Step._authorizeTransferApproval 4 - RuleConditionalTransferLightOwnable2Step.supportsInterface - 7 + RuleConditionalTransferLightOwnable2Step.supportsInterface + 12
diff --git a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol.func.html b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol.func.html index cc07253..bc819ef 100644 --- a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol.func.html +++ b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol.func.html @@ -31,18 +31,18 @@ lcov.info Lines: + 8 9 - 9 - 100.0 % + 88.9 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 3 - 3 - 100.0 % + 4 + 75.0 % @@ -69,16 +69,20 @@ Hit count Sort by hit count - RuleConditionalTransferLightOwnable2Step._authorizeTransferApproval + RuleConditionalTransferLightOwnable2Step._authorizeComplianceBindingChange + 0 + + + RuleConditionalTransferLightOwnable2Step._authorizeTransferApproval 4 - RuleConditionalTransferLightOwnable2Step._onlyComplianceManager + RuleConditionalTransferLightOwnable2Step._onlyComplianceManager 3 - RuleConditionalTransferLightOwnable2Step.supportsInterface - 7 + RuleConditionalTransferLightOwnable2Step.supportsInterface + 12
diff --git a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol.gcov.html b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol.gcov.html index 221d711..e9a3429 100644 --- a/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol.gcov.html @@ -31,18 +31,18 @@ lcov.info Lines: + 8 9 - 9 - 100.0 % + 88.9 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 3 - 3 - 100.0 % + 4 + 75.0 % @@ -74,46 +74,72 @@ 3 : : 4 : : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 5 : : import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; - 6 : : import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; - 7 : : import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + 6 : : import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + 7 : : import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; 8 : : import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; 9 : : import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; 10 : : import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; 11 : : import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol"; 12 : : import {RuleConditionalTransferLightBase} from "./abstract/RuleConditionalTransferLightBase.sol"; - 13 : : - 14 : : /** - 15 : : * @title RuleConditionalTransferLightOwnable2Step - 16 : : * @notice Ownable2Step variant of RuleConditionalTransferLight. - 17 : : */ - 18 : : contract RuleConditionalTransferLightOwnable2Step is RuleConditionalTransferLightBase, Ownable2Step { - 19 : : /*////////////////////////////////////////////////////////////// - 20 : : CONSTRUCTOR - 21 : : //////////////////////////////////////////////////////////////*/ - 22 : : - 23 : : constructor(address owner) Ownable(owner) {} - 24 : : - 25 : : /*////////////////////////////////////////////////////////////// - 26 : : PUBLIC FUNCTIONS - 27 : : //////////////////////////////////////////////////////////////*/ - 28 : : - 29 : 7 : function supportsInterface(bytes4 interfaceId) public view override returns (bool) { - 30 : 7 : return interfaceId == type(IERC165).interfaceId - 31 : 6 : || interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID - 32 : 5 : || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID - 33 : 4 : || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID - 34 : 3 : || interfaceId == type(IERC7551Compliance).interfaceId - 35 : 2 : || interfaceId == type(IERC3643ComplianceFull).interfaceId; - 36 : : } - 37 : : - 38 : : /*////////////////////////////////////////////////////////////// - 39 : : ACCESS CONTROL - 40 : : //////////////////////////////////////////////////////////////*/ - 41 : : - 42 : 4 : function _authorizeTransferApproval() internal view virtual override onlyOwner {} - 43 : : - 44 : 3 : function _onlyComplianceManager() internal virtual override onlyOwner {} - 45 : : } + 13 : : import {Ownable2StepERC165Module} from "../../modules/Ownable2StepERC165Module.sol"; + 14 : : + 15 : : /** + 16 : : * @title RuleConditionalTransferLightOwnable2Step + 17 : : * @notice Ownable2Step variant of RuleConditionalTransferLight. + 18 : : */ + 19 : : contract RuleConditionalTransferLightOwnable2Step is + 20 : : RuleConditionalTransferLightBase, + 21 : : Ownable2Step, + 22 : : Ownable2StepERC165Module + 23 : : { + 24 : : /*////////////////////////////////////////////////////////////// + 25 : : CONSTRUCTOR + 26 : : //////////////////////////////////////////////////////////////*/ + 27 : : + 28 : : /** + 29 : : * @param owner Address of the contract owner. + 30 : : */ + 31 : : constructor(address owner) Ownable(owner) {} + 32 : : + 33 : : /*////////////////////////////////////////////////////////////// + 34 : : PUBLIC FUNCTIONS + 35 : : //////////////////////////////////////////////////////////////*/ + 36 : : + 37 : : /** + 38 : : * @inheritdoc IERC165 + 39 : : */ + 40 : 12 : function supportsInterface(bytes4 interfaceId) + 41 : : public + 42 : : view + 43 : : override(Ownable2StepERC165Module, IERC165) + 44 : : returns (bool) + 45 : : { + 46 : 12 : return Ownable2StepERC165Module.supportsInterface(interfaceId) + 47 : 8 : || interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + 48 : 7 : || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + 49 : 6 : || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId + 50 : 4 : || interfaceId == type(IERC3643ComplianceFull).interfaceId; + 51 : : } + 52 : : + 53 : : /*////////////////////////////////////////////////////////////// + 54 : : ACCESS CONTROL + 55 : : //////////////////////////////////////////////////////////////*/ + 56 : : + 57 : : /** + 58 : : * @notice Reverts unless the caller is the owner. + 59 : : */ + 60 : 3 : function _onlyComplianceManager() internal view virtual override onlyOwner {} + 61 : : + 62 : : /** + 63 : : * @notice Reverts unless the caller is the owner. + 64 : : */ + 65 : 4 : function _authorizeTransferApproval() internal view virtual override onlyOwner {} + 66 : : + 67 : : /** + 68 : : * @notice Reverts unless the caller is the owner. + 69 : : */ + 70 : 0 : function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {} + 71 : : } diff --git a/doc/coverage/coverage/src/rules/operation/RuleMintAllowance.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/operation/RuleMintAllowance.sol.func-sort-c.html new file mode 100644 index 0000000..86e0753 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/RuleMintAllowance.sol.func-sort-c.html @@ -0,0 +1,97 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/RuleMintAllowance.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation - RuleMintAllowance.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:88100.0 %
Date:2026-07-14 13:44:06Functions:44100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleMintAllowance._authorizeComplianceBindingChange4
RuleMintAllowance.supportsInterface34
RuleMintAllowance._onlyComplianceManager312
RuleMintAllowance._authorizeSetMintAllowance10070
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/RuleMintAllowance.sol.func.html b/doc/coverage/coverage/src/rules/operation/RuleMintAllowance.sol.func.html new file mode 100644 index 0000000..1551c0a --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/RuleMintAllowance.sol.func.html @@ -0,0 +1,97 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/RuleMintAllowance.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation - RuleMintAllowance.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:88100.0 %
Date:2026-07-14 13:44:06Functions:44100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleMintAllowance._authorizeComplianceBindingChange4
RuleMintAllowance._authorizeSetMintAllowance10070
RuleMintAllowance._onlyComplianceManager312
RuleMintAllowance.supportsInterface34
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/RuleMintAllowance.sol.gcov.html b/doc/coverage/coverage/src/rules/operation/RuleMintAllowance.sol.gcov.html new file mode 100644 index 0000000..53e1d29 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/RuleMintAllowance.sol.gcov.html @@ -0,0 +1,162 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/RuleMintAllowance.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation - RuleMintAllowance.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:88100.0 %
Date:2026-07-14 13:44:06Functions:44100.0 %
Branches:00-
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : pragma solidity ^0.8.20;
+       3                 :            : 
+       4                 :            : import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol";
+       5                 :            : import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+       6                 :            : import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol";
+       7                 :            : import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol";
+       8                 :            : import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol";
+       9                 :            : import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
+      10                 :            : import {AccessControlModuleStandalone} from "../../modules/AccessControlModuleStandalone.sol";
+      11                 :            : import {RuleMintAllowanceBase} from "./abstract/RuleMintAllowanceBase.sol";
+      12                 :            : import {ERC3643ComplianceRolesStorage} from "RuleEngine/modules/library/ERC3643ComplianceRolesStorage.sol";
+      13                 :            : 
+      14                 :            : /**
+      15                 :            :  * @title RuleMintAllowance
+      16                 :            :  * @notice AccessControl variant of RuleMintAllowance.
+      17                 :            :  *         `DEFAULT_ADMIN_ROLE` implicitly holds all roles.
+      18                 :            :  *         `ALLOWANCE_OPERATOR_ROLE` can set, increase, and decrease per-minter allowances.
+      19                 :            :  *         `COMPLIANCE_MANAGER_ROLE` can bind/unbind the rule to a RuleEngine.
+      20                 :            :  */
+      21                 :            : contract RuleMintAllowance is AccessControlModuleStandalone, RuleMintAllowanceBase, ERC3643ComplianceRolesStorage {
+      22                 :            :     /*//////////////////////////////////////////////////////////////
+      23                 :            :                              CONSTRUCTOR
+      24                 :            :     //////////////////////////////////////////////////////////////*/
+      25                 :            : 
+      26                 :            :     /**
+      27                 :            :      * @param admin Address of the contract admin.
+      28                 :            :      */
+      29                 :            :     constructor(address admin) AccessControlModuleStandalone(admin) {}
+      30                 :            : 
+      31                 :            :     /*//////////////////////////////////////////////////////////////
+      32                 :            :                           PUBLIC FUNCTIONS
+      33                 :            :     //////////////////////////////////////////////////////////////*/
+      34                 :            : 
+      35                 :            :     /**
+      36                 :            :      * @inheritdoc IERC165
+      37                 :            :      */
+      38                 :         34 :     function supportsInterface(bytes4 interfaceId)
+      39                 :            :         public
+      40                 :            :         view
+      41                 :            :         virtual
+      42                 :            :         override(AccessControlEnumerable, IERC165)
+      43                 :            :         returns (bool)
+      44                 :            :     {
+      45                 :            :         // Do not advertise full ERC-3643 ICompliance: its 3-arg mint callback
+      46                 :            :         // cannot identify the minter, so quota enforcement needs the spender-aware path.
+      47                 :         34 :         return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID
+      48                 :         33 :             || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID
+      49                 :         32 :             || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId
+      50                 :         21 :             || AccessControlEnumerable.supportsInterface(interfaceId);
+      51                 :            :     }
+      52                 :            : 
+      53                 :            :     /*//////////////////////////////////////////////////////////////
+      54                 :            :                             ACCESS CONTROL
+      55                 :            :     //////////////////////////////////////////////////////////////*/
+      56                 :            : 
+      57                 :            :     /**
+      58                 :            :      * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`.
+      59                 :            :      */
+      60                 :        312 :     function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {}
+      61                 :            : 
+      62                 :            :     /**
+      63                 :            :      * @notice Reverts unless the caller holds `ALLOWANCE_OPERATOR_ROLE`.
+      64                 :            :      */
+      65                 :      10070 :     function _authorizeSetMintAllowance() internal view virtual override onlyRole(ALLOWANCE_OPERATOR_ROLE) {}
+      66                 :            : 
+      67                 :            :     /**
+      68                 :            :      * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`.
+      69                 :            :      */
+      70                 :          4 :     function _authorizeComplianceBindingChange(address)
+      71                 :            :         internal
+      72                 :            :         view
+      73                 :            :         virtual
+      74                 :            :         override
+      75                 :            :         onlyRole(COMPLIANCE_MANAGER_ROLE)
+      76                 :            :     {}
+      77                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/RuleMintAllowanceOwnable2Step.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/operation/RuleMintAllowanceOwnable2Step.sol.func-sort-c.html new file mode 100644 index 0000000..f2353af --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/RuleMintAllowanceOwnable2Step.sol.func-sort-c.html @@ -0,0 +1,97 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/RuleMintAllowanceOwnable2Step.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation - RuleMintAllowanceOwnable2Step.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:88100.0 %
Date:2026-07-14 13:44:06Functions:44100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleMintAllowanceOwnable2Step._authorizeComplianceBindingChange2
RuleMintAllowanceOwnable2Step._authorizeSetMintAllowance6
RuleMintAllowanceOwnable2Step.supportsInterface8
RuleMintAllowanceOwnable2Step._onlyComplianceManager11
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/RuleMintAllowanceOwnable2Step.sol.func.html b/doc/coverage/coverage/src/rules/operation/RuleMintAllowanceOwnable2Step.sol.func.html new file mode 100644 index 0000000..a7f4346 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/RuleMintAllowanceOwnable2Step.sol.func.html @@ -0,0 +1,97 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/RuleMintAllowanceOwnable2Step.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation - RuleMintAllowanceOwnable2Step.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:88100.0 %
Date:2026-07-14 13:44:06Functions:44100.0 %
Branches:00-
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleMintAllowanceOwnable2Step._authorizeComplianceBindingChange2
RuleMintAllowanceOwnable2Step._authorizeSetMintAllowance6
RuleMintAllowanceOwnable2Step._onlyComplianceManager11
RuleMintAllowanceOwnable2Step.supportsInterface8
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/RuleMintAllowanceOwnable2Step.sol.gcov.html b/doc/coverage/coverage/src/rules/operation/RuleMintAllowanceOwnable2Step.sol.gcov.html new file mode 100644 index 0000000..cc4d4b8 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/RuleMintAllowanceOwnable2Step.sol.gcov.html @@ -0,0 +1,153 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/RuleMintAllowanceOwnable2Step.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation - RuleMintAllowanceOwnable2Step.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:88100.0 %
Date:2026-07-14 13:44:06Functions:44100.0 %
Branches:00-
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : pragma solidity ^0.8.20;
+       3                 :            : 
+       4                 :            : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
+       5                 :            : import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
+       6                 :            : import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
+       7                 :            : import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol";
+       8                 :            : import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol";
+       9                 :            : import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol";
+      10                 :            : import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
+      11                 :            : import {RuleMintAllowanceBase} from "./abstract/RuleMintAllowanceBase.sol";
+      12                 :            : import {Ownable2StepERC165Module} from "../../modules/Ownable2StepERC165Module.sol";
+      13                 :            : 
+      14                 :            : /**
+      15                 :            :  * @title RuleMintAllowanceOwnable2Step
+      16                 :            :  * @notice Ownable2Step variant of RuleMintAllowance.
+      17                 :            :  *         The owner manages all allowances and compliance bindings.
+      18                 :            :  */
+      19                 :            : contract RuleMintAllowanceOwnable2Step is RuleMintAllowanceBase, Ownable2Step, Ownable2StepERC165Module {
+      20                 :            :     /*//////////////////////////////////////////////////////////////
+      21                 :            :                              CONSTRUCTOR
+      22                 :            :     //////////////////////////////////////////////////////////////*/
+      23                 :            : 
+      24                 :            :     /**
+      25                 :            :      * @param owner Address of the contract owner.
+      26                 :            :      */
+      27                 :            :     constructor(address owner) Ownable(owner) {}
+      28                 :            : 
+      29                 :            :     /*//////////////////////////////////////////////////////////////
+      30                 :            :                           PUBLIC FUNCTIONS
+      31                 :            :     //////////////////////////////////////////////////////////////*/
+      32                 :            : 
+      33                 :            :     /**
+      34                 :            :      * @inheritdoc IERC165
+      35                 :            :      */
+      36                 :          8 :     function supportsInterface(bytes4 interfaceId)
+      37                 :            :         public
+      38                 :            :         view
+      39                 :            :         override(Ownable2StepERC165Module, IERC165)
+      40                 :            :         returns (bool)
+      41                 :            :     {
+      42                 :            :         // Do not advertise full ERC-3643 ICompliance: its 3-arg mint callback
+      43                 :            :         // cannot identify the minter, so quota enforcement needs the spender-aware path.
+      44                 :          8 :         return Ownable2StepERC165Module.supportsInterface(interfaceId)
+      45                 :          6 :             || interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID
+      46                 :          5 :             || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID
+      47                 :          4 :             || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId;
+      48                 :            :     }
+      49                 :            : 
+      50                 :            :     /*//////////////////////////////////////////////////////////////
+      51                 :            :                             ACCESS CONTROL
+      52                 :            :     //////////////////////////////////////////////////////////////*/
+      53                 :            : 
+      54                 :            :     /**
+      55                 :            :      * @notice Reverts unless the caller is the owner.
+      56                 :            :      */
+      57                 :         11 :     function _onlyComplianceManager() internal view virtual override onlyOwner {}
+      58                 :            : 
+      59                 :            :     /**
+      60                 :            :      * @notice Reverts unless the caller is the owner.
+      61                 :            :      */
+      62                 :          6 :     function _authorizeSetMintAllowance() internal view virtual override onlyOwner {}
+      63                 :            : 
+      64                 :            :     /**
+      65                 :            :      * @notice Reverts unless the caller is the owner.
+      66                 :            :      */
+      67                 :          2 :     function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {}
+      68                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol.func-sort-c.html index 58e7df6..7820ea3 100644 --- a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol.func-sort-c.html @@ -31,26 +31,26 @@ lcov.info Lines: - 35 - 37 - 94.6 % + 41 + 43 + 95.3 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 9 - 11 - 81.8 % + 10 + 12 + 83.3 % Branches: - 5 - 5 + 7 + 7 100.0 % @@ -69,48 +69,52 @@ Hit count Sort by hit count - RuleConditionalTransferLightApprovalBase._authorizeTransferApproval + RuleConditionalTransferLightApprovalBase._authorizeTransferApproval 0 - RuleConditionalTransferLightApprovalBase._authorizeTransferExecution + RuleConditionalTransferLightApprovalBase._authorizeTransferExecution 0 - RuleConditionalTransferLightApprovalBase._transferredFromContext + RuleConditionalTransferLightApprovalBase._transferredFromContext 3 - RuleConditionalTransferLightApprovalBase.onlyTransferExecutor + RuleConditionalTransferLightApprovalBase.onlyTransferExecutor 3 - RuleConditionalTransferLightApprovalBase.transferred + RuleConditionalTransferLightApprovalBase.transferred 3 - RuleConditionalTransferLightApprovalBase.cancelTransferApproval + RuleConditionalTransferLightApprovalBase.onlyTransferApprover 4 - RuleConditionalTransferLightApprovalBase.onlyTransferApprover + RuleConditionalTransferLightApprovalBase.resetApproval 4 - RuleConditionalTransferLightApprovalBase.approvedCount - 263 + RuleConditionalTransferLightApprovalBase.cancelTransferApproval + 1386 - RuleConditionalTransferLightApprovalBase._transferred - 850 + RuleConditionalTransferLightApprovalBase.approveTransfer + 6210 - RuleConditionalTransferLightApprovalBase.approveTransfer - 1917 + RuleConditionalTransferLightApprovalBase._transferred + 6353 - RuleConditionalTransferLightApprovalBase._transferHash - 3039 + RuleConditionalTransferLightApprovalBase.approvedCount + 9207 + + + RuleConditionalTransferLightApprovalBase._transferHash + 19124
diff --git a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol.func.html b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol.func.html index eed496c..25cd8a9 100644 --- a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol.func.html +++ b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol.func.html @@ -31,26 +31,26 @@ lcov.info Lines: - 35 - 37 - 94.6 % + 41 + 43 + 95.3 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 9 - 11 - 81.8 % + 10 + 12 + 83.3 % Branches: - 5 - 5 + 7 + 7 100.0 % @@ -69,47 +69,51 @@ Hit count Sort by hit count - RuleConditionalTransferLightApprovalBase._authorizeTransferApproval + RuleConditionalTransferLightApprovalBase._authorizeTransferApproval 0 - RuleConditionalTransferLightApprovalBase._authorizeTransferExecution + RuleConditionalTransferLightApprovalBase._authorizeTransferExecution 0 - RuleConditionalTransferLightApprovalBase._transferHash - 3039 + RuleConditionalTransferLightApprovalBase._transferHash + 19124 - RuleConditionalTransferLightApprovalBase._transferred - 850 + RuleConditionalTransferLightApprovalBase._transferred + 6353 - RuleConditionalTransferLightApprovalBase._transferredFromContext + RuleConditionalTransferLightApprovalBase._transferredFromContext 3 - RuleConditionalTransferLightApprovalBase.approveTransfer - 1917 + RuleConditionalTransferLightApprovalBase.approveTransfer + 6210 - RuleConditionalTransferLightApprovalBase.approvedCount - 263 + RuleConditionalTransferLightApprovalBase.approvedCount + 9207 - RuleConditionalTransferLightApprovalBase.cancelTransferApproval - 4 + RuleConditionalTransferLightApprovalBase.cancelTransferApproval + 1386 - RuleConditionalTransferLightApprovalBase.onlyTransferApprover + RuleConditionalTransferLightApprovalBase.onlyTransferApprover 4 - RuleConditionalTransferLightApprovalBase.onlyTransferExecutor + RuleConditionalTransferLightApprovalBase.onlyTransferExecutor 3 - RuleConditionalTransferLightApprovalBase.transferred + RuleConditionalTransferLightApprovalBase.resetApproval + 4 + + + RuleConditionalTransferLightApprovalBase.transferred 3 diff --git a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol.gcov.html b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol.gcov.html index 2388ee3..31677e1 100644 --- a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol.gcov.html @@ -31,26 +31,26 @@ lcov.info Lines: - 35 - 37 - 94.6 % + 41 + 43 + 95.3 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 9 - 11 - 81.8 % + 10 + 12 + 83.3 % Branches: - 5 - 5 + 7 + 7 100.0 % @@ -81,90 +81,164 @@ 10 : : * No knowledge of token binding or compliance interfaces. 11 : : */ 12 : : abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTransferLightInvariantStorage { - 13 : : // Mapping from transfer hash to approval count - 14 : : mapping(bytes32 => uint256) public approvalCounts; - 15 : : - 16 : : /*////////////////////////////////////////////////////////////// - 17 : : ACCESS CONTROL - 18 : : //////////////////////////////////////////////////////////////*/ - 19 : : - 20 : 4 : modifier onlyTransferApprover() { - 21 : 4 : _authorizeTransferApproval(); - 22 : : _; - 23 : : } - 24 : : - 25 : 3 : modifier onlyTransferExecutor() { - 26 : 3 : _authorizeTransferExecution(); - 27 : : _; - 28 : : } - 29 : : - 30 : 0 : function _authorizeTransferApproval() internal view virtual; + 13 : : /** + 14 : : * @notice Number of outstanding approvals for each transfer hash (mapping from transfer hash to approval count) + 15 : : */ + 16 : : mapping(bytes32 => uint256) public approvalCounts; + 17 : : + 18 : : /*////////////////////////////////////////////////////////////// + 19 : : ACCESS CONTROL + 20 : : //////////////////////////////////////////////////////////////*/ + 21 : : + 22 : 4 : modifier onlyTransferApprover() { + 23 : 4 : _authorizeTransferApproval(); + 24 : : _; + 25 : : } + 26 : : + 27 : 3 : modifier onlyTransferExecutor() { + 28 : 3 : _authorizeTransferExecution(); + 29 : : _; + 30 : : } 31 : : - 32 : 0 : function _authorizeTransferExecution() internal view virtual; - 33 : : - 34 : : /*////////////////////////////////////////////////////////////// - 35 : : EXTERNAL FUNCTIONS - 36 : : //////////////////////////////////////////////////////////////*/ - 37 : : - 38 : 3 : function transferred(ITransferContext.FungibleTransferContext calldata ctx) external onlyTransferExecutor { - 39 : 3 : _transferredFromContext(ctx); - 40 : : } - 41 : : - 42 : : /*////////////////////////////////////////////////////////////// - 43 : : PUBLIC FUNCTIONS - 44 : : //////////////////////////////////////////////////////////////*/ - 45 : : - 46 : 1917 : function approveTransfer(address from, address to, uint256 value) public onlyTransferApprover { - 47 : 1918 : bytes32 transferHash = _transferHash(from, to, value); - 48 : 1918 : approvalCounts[transferHash] += 1; - 49 : 1918 : emit TransferApproved(from, to, value, approvalCounts[transferHash]); - 50 : : } - 51 : : - 52 : 4 : function cancelTransferApproval(address from, address to, uint256 value) public onlyTransferApprover { - 53 : 3 : bytes32 transferHash = _transferHash(from, to, value); - 54 : 3 : uint256 count = approvalCounts[transferHash]; - 55 [ + + ]: 3 : require(count != 0, TransferApprovalNotFound()); - 56 : 2 : approvalCounts[transferHash] = count - 1; - 57 : 2 : emit TransferApprovalCancelled(from, to, value, approvalCounts[transferHash]); + 32 : : /*////////////////////////////////////////////////////////////// + 33 : : EXTERNAL FUNCTIONS + 34 : : //////////////////////////////////////////////////////////////*/ + 35 : : + 36 : : /** + 37 : : * @notice Consumes one approval for the transfer described by `ctx`. + 38 : : * @param ctx The fungible transfer context (from, to, value). + 39 : : */ + 40 : 3 : function transferred(ITransferContext.FungibleTransferContext calldata ctx) external onlyTransferExecutor { + 41 : 3 : _transferredFromContext(ctx); + 42 : : } + 43 : : + 44 : : /*////////////////////////////////////////////////////////////// + 45 : : PUBLIC FUNCTIONS + 46 : : //////////////////////////////////////////////////////////////*/ + 47 : : + 48 : : /** + 49 : : * @notice Records a new approval for the given transfer, incrementing its approval count. + 50 : : * @param from The sender of the transfer to approve. + 51 : : * @param to The recipient of the transfer to approve. + 52 : : * @param value The amount of the transfer to approve. + 53 : : */ + 54 : 6210 : function approveTransfer(address from, address to, uint256 value) public onlyTransferApprover { + 55 : 6213 : bytes32 transferHash = _transferHash(from, to, value); + 56 : 6213 : approvalCounts[transferHash] += 1; + 57 : 6213 : emit TransferApproved(from, to, value, approvalCounts[transferHash]); 58 : : } 59 : : - 60 : 263 : function approvedCount(address from, address to, uint256 value) public view returns (uint256) { - 61 : 263 : bytes32 transferHash = _transferHash(from, to, value); - 62 : 263 : return approvalCounts[transferHash]; - 63 : : } - 64 : : - 65 : : /*////////////////////////////////////////////////////////////// - 66 : : INTERNAL FUNCTIONS - 67 : : //////////////////////////////////////////////////////////////*/ - 68 : : - 69 : 3 : function _transferredFromContext(ITransferContext.FungibleTransferContext calldata ctx) internal virtual { - 70 : 3 : _transferred(ctx.from, ctx.to, ctx.value); - 71 : : } - 72 : : - 73 : 850 : function _transferred(address from, address to, uint256 value) internal virtual { - 74 [ + ]: 850 : if (from == address(0) || to == address(0)) { - 75 : 850 : return; - 76 : : } - 77 : 846 : bytes32 transferHash = _transferHash(from, to, value); - 78 : 846 : uint256 count = approvalCounts[transferHash]; - 79 : : - 80 [ + + ]: 846 : require(count != 0, TransferNotApproved()); - 81 : : - 82 : 843 : approvalCounts[transferHash] = count - 1; - 83 : 843 : emit TransferExecuted(from, to, value, approvalCounts[transferHash]); - 84 : : } - 85 : : - 86 : 3039 : function _transferHash(address from, address to, uint256 value) internal pure virtual returns (bytes32 hash) { - 87 : : // Linter suggestion (`asm-keccak256`): hash packed values in assembly to avoid abi.encodePacked overhead. - 88 : : assembly ("memory-safe") { - 89 : 3039 : let ptr := mload(0x40) - 90 : 3039 : mstore(ptr, shl(96, from)) - 91 : 3039 : mstore(add(ptr, 0x20), shl(96, to)) - 92 : 3039 : mstore(add(ptr, 0x40), value) - 93 : 3039 : hash := keccak256(ptr, 0x60) - 94 : : } - 95 : : } - 96 : : } + 60 : : /** + 61 : : * @notice Cancels one outstanding approval for the given transfer; reverts if none exists. + 62 : : * @param from The sender of the transfer whose approval is cancelled. + 63 : : * @param to The recipient of the transfer whose approval is cancelled. + 64 : : * @param value The amount of the transfer whose approval is cancelled. + 65 : : */ + 66 : 1386 : function cancelTransferApproval(address from, address to, uint256 value) public onlyTransferApprover { + 67 : 1385 : bytes32 transferHash = _transferHash(from, to, value); + 68 : 1385 : uint256 count = approvalCounts[transferHash]; + 69 [ + + ]: 1385 : require(count != 0, TransferApprovalNotFound()); + 70 : 1384 : approvalCounts[transferHash] = count - 1; + 71 : 1384 : emit TransferApprovalCancelled(from, to, value, approvalCounts[transferHash]); + 72 : : } + 73 : : + 74 : : /** + 75 : : * @notice Discards every outstanding approval for the given transfer in one call. + 76 : : * @dev + 77 : : * - Reverts if no approval exists, per the single-item convention (use {cancelTransferApproval} + 78 : : * to remove exactly one). + 79 : : * - Deliberately does NOT require a bound token: the primary use is cleaning up approvals that + 80 : : * survived an {unbindToken}, at which point no token is bound. See the {bindToken} warning. + 81 : : * @param from The sender of the transfer whose approvals are cleared. + 82 : : * @param to The recipient of the transfer whose approvals are cleared. + 83 : : * @param value The amount of the transfer whose approvals are cleared. + 84 : : * @return cleared The approval count that was discarded. + 85 : : */ + 86 : 4 : function resetApproval(address from, address to, uint256 value) + 87 : : public + 88 : : virtual + 89 : : onlyTransferApprover + 90 : : returns (uint256 cleared) + 91 : : { + 92 : 3 : bytes32 transferHash = _transferHash(from, to, value); + 93 : 3 : cleared = approvalCounts[transferHash]; + 94 [ + + ]: 3 : require(cleared != 0, TransferApprovalNotFound()); + 95 : 2 : approvalCounts[transferHash] = 0; + 96 : 2 : emit TransferApprovalReset(from, to, value, cleared); + 97 : : } + 98 : : + 99 : : /** + 100 : : * @notice Returns the number of outstanding approvals for the given transfer. + 101 : : * @param from The sender of the transfer. + 102 : : * @param to The recipient of the transfer. + 103 : : * @param value The amount of the transfer. + 104 : : * @return The current approval count for the transfer. + 105 : : */ + 106 : 9207 : function approvedCount(address from, address to, uint256 value) public view returns (uint256) { + 107 : 9207 : bytes32 transferHash = _transferHash(from, to, value); + 108 : 9207 : return approvalCounts[transferHash]; + 109 : : } + 110 : : + 111 : : /*////////////////////////////////////////////////////////////// + 112 : : INTERNAL FUNCTIONS + 113 : : //////////////////////////////////////////////////////////////*/ + 114 : : + 115 : : /** + 116 : : * @notice Consumes one approval for the transfer described by `ctx`. + 117 : : * @param ctx The fungible transfer context (from, to, value). + 118 : : */ + 119 : 3 : function _transferredFromContext(ITransferContext.FungibleTransferContext calldata ctx) internal virtual { + 120 : 3 : _transferred(ctx.from, ctx.to, ctx.value); + 121 : : } + 122 : : + 123 : : /** + 124 : : * @notice Consumes one approval for the given transfer; reverts if none exists. + 125 : : * @dev No-op when either endpoint is the zero address (mint/burn). + 126 : : * @param from The sender of the transfer. + 127 : : * @param to The recipient of the transfer. + 128 : : * @param value The amount of the transfer. + 129 : : */ + 130 : 6353 : function _transferred(address from, address to, uint256 value) internal virtual { + 131 [ + ]: 6353 : if (from == address(0) || to == address(0)) { + 132 : 6353 : return; + 133 : : } + 134 : 2307 : bytes32 transferHash = _transferHash(from, to, value); + 135 : 2307 : uint256 count = approvalCounts[transferHash]; + 136 : : + 137 [ + + ]: 2307 : require(count != 0, TransferNotApproved()); + 138 : : + 139 : 2302 : approvalCounts[transferHash] = count - 1; + 140 : 2302 : emit TransferExecuted(from, to, value, approvalCounts[transferHash]); + 141 : : } + 142 : : + 143 : : /** + 144 : : * @notice Computes the storage key identifying a (from, to, value) transfer. + 145 : : * @param from The sender of the transfer. + 146 : : * @param to The recipient of the transfer. + 147 : : * @param value The amount of the transfer. + 148 : : * @return hash The keccak256 hash uniquely identifying the transfer. + 149 : : */ + 150 : 19124 : function _transferHash(address from, address to, uint256 value) internal pure virtual returns (bytes32 hash) { + 151 : : // Linter suggestion (`asm-keccak256`): hash packed values in assembly to avoid abi.encodePacked overhead. + 152 : : assembly ("memory-safe") { + 153 : 19124 : let ptr := mload(0x40) + 154 : 19124 : mstore(ptr, shl(96, from)) + 155 : 19124 : mstore(add(ptr, 0x20), shl(96, to)) + 156 : 19124 : mstore(add(ptr, 0x40), value) + 157 : 19124 : hash := keccak256(ptr, 0x60) + 158 : : } + 159 : : } + 160 : : + 161 : : /** + 162 : : * @notice Authorizes the caller to approve or cancel transfers; reverts if unauthorized. + 163 : : */ + 164 : 0 : function _authorizeTransferApproval() internal view virtual; + 165 : : + 166 : : /** + 167 : : * @notice Authorizes the caller to execute (consume) approved transfers; reverts if unauthorized. + 168 : : */ + 169 : 0 : function _authorizeTransferExecution() internal view virtual; + 170 : : } diff --git a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol.func-sort-c.html index bfc1a7f..fe08918 100644 --- a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 40 - 40 + 52 + 52 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 13 - 13 + 16 + 16 100.0 % @@ -49,8 +49,8 @@ Branches: - 11 - 11 + 17 + 17 100.0 % @@ -69,56 +69,68 @@ Hit count Sort by hit count - RuleConditionalTransferLightBase.canReturnTransferRestrictionCode + RuleConditionalTransferLightBase.canReturnTransferRestrictionCode 1 - RuleConditionalTransferLightBase.canTransferFrom + RuleConditionalTransferLightBase.canTransferFrom 1 - RuleConditionalTransferLightBase.created + RuleConditionalTransferLightBase.created 1 - RuleConditionalTransferLightBase.destroyed + RuleConditionalTransferLightBase.destroyed 1 - RuleConditionalTransferLightBase.detectTransferRestrictionFrom + RuleConditionalTransferLightBase.detectTransferRestrictionFrom 1 - RuleConditionalTransferLightBase.transferred.1 - 1 + RuleConditionalTransferLightBase.messageForTransferRestriction + 2 - RuleConditionalTransferLightBase.messageForTransferRestriction - 2 + RuleConditionalTransferLightBase.unbindRuleEngine + 3 - RuleConditionalTransferLightBase.approveAndTransferIfAllowed + RuleConditionalTransferLightBase.canTransfer 4 - RuleConditionalTransferLightBase.canTransfer - 4 + RuleConditionalTransferLightBase.approveAndTransferIfAllowed + 6 - RuleConditionalTransferLightBase.detectTransferRestriction + RuleConditionalTransferLightBase.detectTransferRestriction 7 - RuleConditionalTransferLightBase.bindToken - 27 + RuleConditionalTransferLightBase.transferred.1 + 7 + + + RuleConditionalTransferLightBase.isTransferExecutor + 8 + + + RuleConditionalTransferLightBase.bindRuleEngine + 13 + + + RuleConditionalTransferLightBase.bindToken + 45 - RuleConditionalTransferLightBase.transferred.0 - 846 + RuleConditionalTransferLightBase.transferred.0 + 6347 - RuleConditionalTransferLightBase._authorizeTransferExecution - 850 + RuleConditionalTransferLightBase._authorizeTransferExecution + 6357
diff --git a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol.func.html b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol.func.html index 3893954..f808c1e 100644 --- a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol.func.html +++ b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 40 - 40 + 52 + 52 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 13 - 13 + 16 + 16 100.0 % @@ -49,8 +49,8 @@ Branches: - 11 - 11 + 17 + 17 100.0 % @@ -69,56 +69,68 @@ Hit count Sort by hit count - RuleConditionalTransferLightBase._authorizeTransferExecution - 850 + RuleConditionalTransferLightBase._authorizeTransferExecution + 6357 - RuleConditionalTransferLightBase.approveAndTransferIfAllowed - 4 + RuleConditionalTransferLightBase.approveAndTransferIfAllowed + 6 + + + RuleConditionalTransferLightBase.bindRuleEngine + 13 - RuleConditionalTransferLightBase.bindToken - 27 + RuleConditionalTransferLightBase.bindToken + 45 - RuleConditionalTransferLightBase.canReturnTransferRestrictionCode + RuleConditionalTransferLightBase.canReturnTransferRestrictionCode 1 - RuleConditionalTransferLightBase.canTransfer + RuleConditionalTransferLightBase.canTransfer 4 - RuleConditionalTransferLightBase.canTransferFrom + RuleConditionalTransferLightBase.canTransferFrom 1 - RuleConditionalTransferLightBase.created + RuleConditionalTransferLightBase.created 1 - RuleConditionalTransferLightBase.destroyed + RuleConditionalTransferLightBase.destroyed 1 - RuleConditionalTransferLightBase.detectTransferRestriction + RuleConditionalTransferLightBase.detectTransferRestriction 7 - RuleConditionalTransferLightBase.detectTransferRestrictionFrom + RuleConditionalTransferLightBase.detectTransferRestrictionFrom 1 - RuleConditionalTransferLightBase.messageForTransferRestriction + RuleConditionalTransferLightBase.isTransferExecutor + 8 + + + RuleConditionalTransferLightBase.messageForTransferRestriction 2 - RuleConditionalTransferLightBase.transferred.0 - 846 + RuleConditionalTransferLightBase.transferred.0 + 6347 - RuleConditionalTransferLightBase.transferred.1 - 1 + RuleConditionalTransferLightBase.transferred.1 + 7 + + + RuleConditionalTransferLightBase.unbindRuleEngine + 3
diff --git a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol.gcov.html b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol.gcov.html index 2084755..ecd7264 100644 --- a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 40 - 40 + 52 + 52 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 13 - 13 + 16 + 16 100.0 % @@ -49,8 +49,8 @@ Branches: - 11 - 11 + 17 + 17 100.0 % @@ -97,154 +97,289 @@ 26 : : using SafeERC20 for IERC20; 27 : : 28 : : /*////////////////////////////////////////////////////////////// - 29 : : EXTERNAL FUNCTIONS + 29 : : STATE 30 : : //////////////////////////////////////////////////////////////*/ 31 : : - 32 : 1 : function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { - 33 : 1 : return restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED; - 34 : : } - 35 : : - 36 : 2 : function messageForTransferRestriction(uint8 restrictionCode) - 37 : : external - 38 : : pure - 39 : : override(IERC1404) - 40 : : returns (string memory) - 41 : : { - 42 [ + ]: 2 : if (restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED) { - 43 : 1 : return TEXT_TRANSFER_REQUEST_NOT_APPROVED; - 44 : : } - 45 : 1 : return TEXT_CODE_NOT_FOUND; - 46 : : } - 47 : : - 48 : 1 : function created(address to, uint256 value) external onlyBoundToken { - 49 : 1 : _transferred(address(0), to, value); - 50 : : } - 51 : : - 52 : 1 : function destroyed(address from, uint256 value) external onlyBoundToken { - 53 : 1 : _transferred(from, address(0), value); - 54 : : } - 55 : : - 56 : : /*////////////////////////////////////////////////////////////// - 57 : : PUBLIC FUNCTIONS - 58 : : //////////////////////////////////////////////////////////////*/ - 59 : : - 60 : : /** - 61 : : * @notice Approves and performs a transferFrom using this rule as spender. - 62 : : * @dev Requires `from` to have approved this contract on the token. - 63 : : * @dev This function is only safe for tokens that call back `transferred()` during transfer. - 64 : : * @dev CEI is intentionally inverted so the approval exists for the callback. - 65 : : */ - 66 : 4 : function approveAndTransferIfAllowed(address from, address to, uint256 value) - 67 : : public - 68 : : onlyTransferApprover - 69 : : returns (bool) - 70 : : { - 71 : 4 : address token = getTokenBound(); - 72 [ + + ]: 4 : require(token != address(0), RuleConditionalTransferLight_TokenNotBound()); - 73 : : - 74 : 3 : approveTransfer(from, to, value); - 75 : : - 76 : 3 : uint256 allowed = IERC20(token).allowance(from, address(this)); - 77 [ + + ]: 3 : require(allowed >= value, RuleConditionalTransferLight_InsufficientAllowance(token, from, allowed, value)); + 32 : : /** + 33 : : * @notice RuleEngine additionally authorized to call the transfer execution hooks. + 34 : : * @dev Binding is deliberately split into two independent roles: + 35 : : * + 36 : : * - {bindToken} — the **ERC-20 token** this rule acts on. It is the token that + 37 : : * {approveAndTransferIfAllowed} calls `safeTransferFrom` on, and it may + 38 : : * call `transferred` itself (direct-binding topology). + 39 : : * - {bindRuleEngine} — a **RuleEngine** allowed to call `transferred` on this rule. Under the + 40 : : * RuleEngine topology the engine, not the token, is the caller of the + 41 : : * compliance hooks. + 42 : : * + 43 : : * Conflating the two was the bug: with a single slot you had to choose between authorizing + 44 : : * the engine (which broke {approveAndTransferIfAllowed}, since the engine is not an ERC-20) + 45 : : * and pointing at the token (which left the engine unauthorized, reverting every transfer). + 46 : : * Binding both makes the helper usable in either topology. + 47 : : */ + 48 : : address public ruleEngine; + 49 : : + 50 : : /*////////////////////////////////////////////////////////////// + 51 : : EXTERNAL FUNCTIONS + 52 : : //////////////////////////////////////////////////////////////*/ + 53 : : + 54 : : /** + 55 : : * @notice Compliance hook invoked when tokens are created (minted); consumes an approval if applicable. + 56 : : * @param to The recipient of the created tokens. + 57 : : * @param value The amount of tokens created. + 58 : : */ + 59 : 1 : function created(address to, uint256 value) external onlyBoundToken { + 60 : 1 : _transferred(address(0), to, value); + 61 : : } + 62 : : + 63 : : /** + 64 : : * @notice Compliance hook invoked when tokens are destroyed (burned); consumes an approval if applicable. + 65 : : * @param from The holder whose tokens are destroyed. + 66 : : * @param value The amount of tokens destroyed. + 67 : : */ + 68 : 1 : function destroyed(address from, uint256 value) external onlyBoundToken { + 69 : 1 : _transferred(from, address(0), value); + 70 : : } + 71 : : + 72 : : /** + 73 : : * @inheritdoc IRule + 74 : : */ + 75 : 1 : function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { + 76 : 1 : return restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED; + 77 : : } 78 : : - 79 : 2 : IERC20(token).safeTransferFrom(from, to, value); - 80 : 1 : return true; - 81 : : } - 82 : : - 83 : 846 : function transferred(address from, address to, uint256 value) - 84 : : public - 85 : : override(IERC3643IComplianceContract) - 86 : : onlyTransferExecutor + 79 : : /** + 80 : : * @inheritdoc IERC1404 + 81 : : */ + 82 : 2 : function messageForTransferRestriction(uint8 restrictionCode) + 83 : : external + 84 : : pure + 85 : : override(IERC1404) + 86 : : returns (string memory) 87 : : { - 88 : 844 : _transferred(from, to, value); - 89 : : } - 90 : : - 91 : 1 : function transferred( - 92 : : address, - 93 : : /* spender */ - 94 : : address from, - 95 : : address to, - 96 : : uint256 value - 97 : : ) - 98 : : public - 99 : : override(IRuleEngine) - 100 : : onlyTransferExecutor - 101 : : { - 102 : 1 : _transferred(from, to, value); - 103 : : } - 104 : : - 105 : : /** - 106 : : * @notice Binds a token to this rule. Reverts if a token is already bound. - 107 : : * @dev Enforces single-token binding to prevent cross-token approval replay. - 108 : : * To migrate to a new token, call `unbindToken` first. - 109 : : * @dev WARNING: `unbindToken` does not clear `approvalCounts`. Stale approvals - 110 : : * from the previous token remain in storage and can be consumed after rebinding. - 111 : : * The operator who controls rebinding also controls approvals, so the trust - 112 : : * model is preserved, but integrators should be aware of this behavior. - 113 : : */ - 114 : 27 : function bindToken(address token) public override onlyComplianceManager { - 115 [ + + ]: 26 : require(getTokenBound() == address(0), RuleConditionalTransferLight_TokenAlreadyBound()); - 116 : 25 : _bindToken(token); - 117 : : } - 118 : : - 119 : 7 : function detectTransferRestriction(address from, address to, uint256 value) - 120 : : public - 121 : : view - 122 : : override(IERC1404) - 123 : : returns (uint8) - 124 : : { - 125 [ + ]: 13 : if (from == address(0) || to == address(0)) { - 126 : 4 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 127 : : } - 128 : 9 : bytes32 transferHash = _transferHash(from, to, value); - 129 [ + ]: 9 : if (approvalCounts[transferHash] == 0) { - 130 : 6 : return CODE_TRANSFER_REQUEST_NOT_APPROVED; - 131 : : } - 132 : 3 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 133 : : } - 134 : : - 135 : 1 : function detectTransferRestrictionFrom( - 136 : : address, - 137 : : /* spender */ - 138 : : address from, - 139 : : address to, - 140 : : uint256 value - 141 : : ) - 142 : : public - 143 : : view - 144 : : override(IERC1404Extend) - 145 : : returns (uint8) - 146 : : { - 147 : 2 : return detectTransferRestriction(from, to, value); - 148 : : } - 149 : : - 150 : 4 : function canTransfer(address from, address to, uint256 value) + 88 [ + ]: 2 : if (restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED) { + 89 : 1 : return TEXT_TRANSFER_REQUEST_NOT_APPROVED; + 90 : : } + 91 : 1 : return TEXT_CODE_NOT_FOUND; + 92 : : } + 93 : : + 94 : : /*////////////////////////////////////////////////////////////// + 95 : : PUBLIC FUNCTIONS + 96 : : //////////////////////////////////////////////////////////////*/ + 97 : : + 98 : : /** + 99 : : * @notice Approves and performs a transferFrom on the bound ERC-20 token, using this rule as spender. + 100 : : * @dev Requires `from` to have approved this contract on the token. + 101 : : * @dev This function is only safe for tokens that call back `transferred()` during transfer. + 102 : : * @dev CEI is intentionally inverted so the approval exists for the callback. + 103 : : * @dev Works in BOTH topologies, provided the bindings are set correctly: + 104 : : * - direct binding: `bindToken(token)` — the token calls back `transferred` itself; + 105 : : * - RuleEngine: `bindToken(token)` AND `bindRuleEngine(engine)` — the engine calls back. + 106 : : * The token must be bound with {bindToken}; binding the RuleEngine there instead would make + 107 : : * `getTokenBound()` a non-ERC-20 and this call would revert. + 108 : : * @param from The holder to transfer tokens from. + 109 : : * @param to The recipient of the transfer. + 110 : : * @param value The amount to transfer. + 111 : : * @return True when the transfer succeeds. + 112 : : */ + 113 : 6 : function approveAndTransferIfAllowed(address from, address to, uint256 value) + 114 : : public + 115 : : onlyTransferApprover + 116 : : returns (bool) + 117 : : { + 118 : 6 : address token = getTokenBound(); + 119 [ + + ]: 6 : require(token != address(0), RuleConditionalTransferLight_TokenNotBound()); + 120 : : + 121 : 5 : approveTransfer(from, to, value); + 122 : : + 123 : 5 : uint256 allowed = IERC20(token).allowance(from, address(this)); + 124 [ + + ]: 4 : require(allowed >= value, RuleConditionalTransferLight_InsufficientAllowance(token, from, allowed, value)); + 125 : : + 126 : 3 : IERC20(token).safeTransferFrom(from, to, value); + 127 : 2 : return true; + 128 : : } + 129 : : + 130 : : /** + 131 : : * @inheritdoc IERC3643IComplianceContract + 132 : : */ + 133 : 6347 : function transferred(address from, address to, uint256 value) + 134 : : public + 135 : : override(IERC3643IComplianceContract) + 136 : : onlyTransferExecutor + 137 : : { + 138 : 6342 : _transferred(from, to, value); + 139 : : } + 140 : : + 141 : : /** + 142 : : * @inheritdoc IRuleEngine + 143 : : */ + 144 : 7 : function transferred( + 145 : : address, + 146 : : /* spender */ + 147 : : address from, + 148 : : address to, + 149 : : uint256 value + 150 : : ) 151 : : public - 152 : : view - 153 : : override(IERC3643ComplianceRead) - 154 : : returns (bool) - 155 : : { - 156 : 4 : return detectTransferRestriction(from, to, value) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 157 : : } - 158 : : - 159 : 1 : function canTransferFrom(address spender, address from, address to, uint256 value) - 160 : : public - 161 : : view - 162 : : override(IERC7551Compliance) - 163 : : returns (bool) - 164 : : { - 165 : 1 : return detectTransferRestrictionFrom(spender, from, to, value) - 166 : : == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 167 : : } - 168 : : - 169 : : /*////////////////////////////////////////////////////////////// - 170 : : ACCESS CONTROL - 171 : : //////////////////////////////////////////////////////////////*/ - 172 : : - 173 : 850 : function _authorizeTransferExecution() internal view override { - 174 [ + + ]: 850 : require(isTokenBound(_msgSender()), RuleConditionalTransferLight_TransferExecutorUnauthorized(_msgSender())); - 175 : : } - 176 : : } + 152 : : override(IRuleEngine) + 153 : : onlyTransferExecutor + 154 : : { + 155 : 6 : _transferred(from, to, value); + 156 : : } + 157 : : + 158 : : /** + 159 : : * @notice Binds the ERC-20 token this rule acts on. Reverts if a token is already bound. + 160 : : * @dev Only ONE token may be bound at a time. To migrate to a new token, call `unbindToken` first. + 161 : : * @dev The bound token is BOTH the ERC-20 that {approveAndTransferIfAllowed} transfers, AND an + 162 : : * authorized caller of `transferred` (the direct-binding topology). If the rule sits behind + 163 : : * a RuleEngine, additionally call {bindRuleEngine} so the engine may call `transferred` too. + 164 : : * @dev ⚠️ Single-token binding alone does NOT guarantee token-scoped approvals: this rule's + 165 : : * approvals are keyed `(from, to, value)` with no token dimension. A multi-tenant + 166 : : * {bindRuleEngine} target would relay several tokens into the same approval bucket — see + 167 : : * the warning on {bindRuleEngine}. + 168 : : * @dev WARNING: `unbindToken` does not clear `approvalCounts`, and does not clear the bound + 169 : : * {ruleEngine} either. Stale approvals from the previous token remain in storage and can be + 170 : : * consumed after rebinding — and the previously bound engine stays authorized to consume + 171 : : * them until {unbindRuleEngine} is called. The operator who controls rebinding also controls + 172 : : * approvals, so the trust model is preserved, but integrators should be aware of this + 173 : : * behavior. When migrating, call {resetApproval} for each affected transfer AND + 174 : : * {unbindRuleEngine} before rebinding. + 175 : : * @param token The ERC-20 token to bind to this rule. + 176 : : */ + 177 : 45 : function bindToken(address token) public override onlyComplianceManager { + 178 [ + + ]: 44 : require(getTokenBound() == address(0), RuleConditionalTransferLight_TokenAlreadyBound()); + 179 : 43 : _bindToken(token); + 180 : : } + 181 : : + 182 : : /** + 183 : : * @notice Authorizes a RuleEngine to call this rule's transfer execution hooks. + 184 : : * @dev Independent of {bindToken}: the engine is authorized to call `transferred`, but is never + 185 : : * treated as the ERC-20 token. Bind the token with {bindToken} and the engine here, and + 186 : : * {approveAndTransferIfAllowed} works under the RuleEngine topology. + 187 : : * Reverts if a RuleEngine is already bound; call {unbindRuleEngine} first to migrate. + 188 : : * + 189 : : * @dev ⚠️ **Bind ONLY an engine that serves this one token.** + 190 : : * This rule's approvals are keyed `(from, to, value)` — they carry **no token dimension**. + 191 : : * A `RuleEngine` is multi-tenant by design (`_boundTokens` is a set), and it relays every + 192 : : * one of its tokens into the same `transferred(from, to, value)` hook, so the rule cannot + 193 : : * tell which token moved. If the bound engine serves several tokens, an approval recorded + 194 : : * for one of them is consumable by ANY of them: + 195 : : * + 196 : : * approveTransfer(alice, bob, 100) // intended for token A + 197 : : * <alice sends 100 of token B> // -> engine -> transferred(alice, bob, 100) + 198 : : * // the token-A approval is consumed + 199 : : * + 200 : : * This is inherent to the single-token rule and is why {RuleConditionalTransferLightMultiToken} + 201 : : * exists. Binding an engine does not change it — it only makes the topology usable, so the + 202 : : * constraint must be respected by the operator. If the engine is (or may become) + 203 : : * multi-tenant, do not use this rule. + 204 : : * + 205 : : * @param ruleEngine_ The RuleEngine allowed to call `transferred`. It MUST serve only the token + 206 : : * bound via {bindToken}. + 207 : : */ + 208 : 13 : function bindRuleEngine(address ruleEngine_) public virtual onlyComplianceManager { + 209 [ + + ]: 12 : require(ruleEngine_ != address(0), RuleConditionalTransferLight_RuleEngineAddressZeroNotAllowed()); + 210 [ + + ]: 11 : require(ruleEngine == address(0), RuleConditionalTransferLight_RuleEngineAlreadyBound()); + 211 : 10 : ruleEngine = ruleEngine_; + 212 : 10 : emit RuleEngineBound(ruleEngine_); + 213 : : } + 214 : : + 215 : : /** + 216 : : * @notice Revokes the bound RuleEngine's authorization to call the transfer execution hooks. + 217 : : * @dev Does NOT clear `approvalCounts` — see the {bindToken} warning and {resetApproval}. + 218 : : */ + 219 : 3 : function unbindRuleEngine() public virtual onlyComplianceManager { + 220 : 2 : address previous = ruleEngine; + 221 [ + + ]: 2 : require(previous != address(0), RuleConditionalTransferLight_RuleEngineNotBound()); + 222 : 1 : ruleEngine = address(0); + 223 : 1 : emit RuleEngineUnbound(previous); + 224 : : } + 225 : : + 226 : : /** + 227 : : * @notice Returns whether `caller` is authorized to call this rule's transfer execution hooks. + 228 : : * @param caller The address to check. + 229 : : * @return True if `caller` is the bound token or the bound RuleEngine. + 230 : : */ + 231 : 8 : function isTransferExecutor(address caller) public view virtual returns (bool) { + 232 : 6365 : return isTokenBound(caller) || (caller != address(0) && caller == ruleEngine); + 233 : : } + 234 : : + 235 : : /** + 236 : : * @inheritdoc IERC1404 + 237 : : */ + 238 : 7 : function detectTransferRestriction(address from, address to, uint256 value) + 239 : : public + 240 : : view + 241 : : override(IERC1404) + 242 : : returns (uint8) + 243 : : { + 244 [ + ]: 13 : if (from == address(0) || to == address(0)) { + 245 : 4 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 246 : : } + 247 : 9 : bytes32 transferHash = _transferHash(from, to, value); + 248 [ + ]: 9 : if (approvalCounts[transferHash] == 0) { + 249 : 6 : return CODE_TRANSFER_REQUEST_NOT_APPROVED; + 250 : : } + 251 : 3 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 252 : : } + 253 : : + 254 : : /** + 255 : : * @inheritdoc IERC1404Extend + 256 : : */ + 257 : 1 : function detectTransferRestrictionFrom( + 258 : : address, + 259 : : /* spender */ + 260 : : address from, + 261 : : address to, + 262 : : uint256 value + 263 : : ) + 264 : : public + 265 : : view + 266 : : override(IERC1404Extend) + 267 : : returns (uint8) + 268 : : { + 269 : 2 : return detectTransferRestriction(from, to, value); + 270 : : } + 271 : : + 272 : : /** + 273 : : * @inheritdoc IERC3643ComplianceRead + 274 : : */ + 275 : 4 : function canTransfer(address from, address to, uint256 value) + 276 : : public + 277 : : view + 278 : : override(IERC3643ComplianceRead) + 279 : : returns (bool) + 280 : : { + 281 : 4 : return detectTransferRestriction(from, to, value) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 282 : : } + 283 : : + 284 : : /** + 285 : : * @inheritdoc IERC7551Compliance + 286 : : */ + 287 : 1 : function canTransferFrom(address spender, address from, address to, uint256 value) + 288 : : public + 289 : : view + 290 : : override(IERC7551Compliance) + 291 : : returns (bool) + 292 : : { + 293 : 1 : return detectTransferRestrictionFrom(spender, from, to, value) + 294 : : == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 295 : : } + 296 : : + 297 : : /*////////////////////////////////////////////////////////////// + 298 : : ACCESS CONTROL + 299 : : //////////////////////////////////////////////////////////////*/ + 300 : : + 301 : : /** + 302 : : * @notice Authorizes transfer execution: the bound token OR the bound RuleEngine may call the + 303 : : * execution hooks. Both topologies are therefore supported without conflating the two + 304 : : * roles of the binding — see {ruleEngine}. + 305 : : */ + 306 : 6357 : function _authorizeTransferExecution() internal view override { + 307 [ + + ]: 6357 : require( + 308 : : isTransferExecutor(_msgSender()), RuleConditionalTransferLight_TransferExecutorUnauthorized(_msgSender()) + 309 : : ); + 310 : : } + 311 : : } diff --git a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol.func-sort-c.html new file mode 100644 index 0000000..d7b4766 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol.func-sort-c.html @@ -0,0 +1,193 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation/abstract - RuleConditionalTransferLightMultiTokenBase.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:919298.9 %
Date:2026-07-14 13:44:06Functions:272896.4 %
Branches:172181.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleConditionalTransferLightMultiTokenBase._authorizeTransferApproval0
RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed1
RuleConditionalTransferLightMultiTokenBase.canTransfer1
RuleConditionalTransferLightMultiTokenBase._cancelTransferApproval2
RuleConditionalTransferLightMultiTokenBase.canReturnTransferRestrictionCode2
RuleConditionalTransferLightMultiTokenBase.canTransferFrom2
RuleConditionalTransferLightMultiTokenBase.created2
RuleConditionalTransferLightMultiTokenBase.destroyed2
RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionFrom2
RuleConditionalTransferLightMultiTokenBase.messageForTransferRestriction2
RuleConditionalTransferLightMultiTokenBase.cancelTransferApproval3
RuleConditionalTransferLightMultiTokenBase.onlyTransferExecutor3
RuleConditionalTransferLightMultiTokenBase.resetApproval3
RuleConditionalTransferLightMultiTokenBase.transferred.03
RuleConditionalTransferLightMultiTokenBase.transferred.14
RuleConditionalTransferLightMultiTokenBase.canTransferForToken5
RuleConditionalTransferLightMultiTokenBase.transferred.26
RuleConditionalTransferLightMultiTokenBase._authorizeTransferExecution13
RuleConditionalTransferLightMultiTokenBase._transferred15
RuleConditionalTransferLightMultiTokenBase.approvedCount16
RuleConditionalTransferLightMultiTokenBase.approveTransfer24
RuleConditionalTransferLightMultiTokenBase.onlyTransferApprover24
RuleConditionalTransferLightMultiTokenBase._approveTransfer25
RuleConditionalTransferLightMultiTokenBase._authorizeComplianceBindingChange38
RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionForToken263
RuleConditionalTransferLightMultiTokenBase.detectTransferRestriction265
RuleConditionalTransferLightMultiTokenBase._detectTransferRestrictionForToken538
RuleConditionalTransferLightMultiTokenBase._transferHash581
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol.func.html b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol.func.html new file mode 100644 index 0000000..1d3f593 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol.func.html @@ -0,0 +1,193 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation/abstract - RuleConditionalTransferLightMultiTokenBase.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:919298.9 %
Date:2026-07-14 13:44:06Functions:272896.4 %
Branches:172181.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleConditionalTransferLightMultiTokenBase._approveTransfer25
RuleConditionalTransferLightMultiTokenBase._authorizeComplianceBindingChange38
RuleConditionalTransferLightMultiTokenBase._authorizeTransferApproval0
RuleConditionalTransferLightMultiTokenBase._authorizeTransferExecution13
RuleConditionalTransferLightMultiTokenBase._cancelTransferApproval2
RuleConditionalTransferLightMultiTokenBase._detectTransferRestrictionForToken538
RuleConditionalTransferLightMultiTokenBase._transferHash581
RuleConditionalTransferLightMultiTokenBase._transferred15
RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed1
RuleConditionalTransferLightMultiTokenBase.approveTransfer24
RuleConditionalTransferLightMultiTokenBase.approvedCount16
RuleConditionalTransferLightMultiTokenBase.canReturnTransferRestrictionCode2
RuleConditionalTransferLightMultiTokenBase.canTransfer1
RuleConditionalTransferLightMultiTokenBase.canTransferForToken5
RuleConditionalTransferLightMultiTokenBase.canTransferFrom2
RuleConditionalTransferLightMultiTokenBase.cancelTransferApproval3
RuleConditionalTransferLightMultiTokenBase.created2
RuleConditionalTransferLightMultiTokenBase.destroyed2
RuleConditionalTransferLightMultiTokenBase.detectTransferRestriction265
RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionForToken263
RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionFrom2
RuleConditionalTransferLightMultiTokenBase.messageForTransferRestriction2
RuleConditionalTransferLightMultiTokenBase.onlyTransferApprover24
RuleConditionalTransferLightMultiTokenBase.onlyTransferExecutor3
RuleConditionalTransferLightMultiTokenBase.resetApproval3
RuleConditionalTransferLightMultiTokenBase.transferred.03
RuleConditionalTransferLightMultiTokenBase.transferred.14
RuleConditionalTransferLightMultiTokenBase.transferred.26
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol.gcov.html b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol.gcov.html new file mode 100644 index 0000000..ba354c7 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol.gcov.html @@ -0,0 +1,541 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation/abstract - RuleConditionalTransferLightMultiTokenBase.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:919298.9 %
Date:2026-07-14 13:44:06Functions:272896.4 %
Branches:172181.0 %
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : pragma solidity ^0.8.20;
+       3                 :            : 
+       4                 :            : import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol";
+       5                 :            : import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol";
+       6                 :            : import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol";
+       7                 :            : import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
+       8                 :            : import {IRule} from "RuleEngine/interfaces/IRule.sol";
+       9                 :            : import {ERC3643ComplianceModule} from "RuleEngine/modules/ERC3643ComplianceModule.sol";
+      10                 :            : import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+      11                 :            : import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+      12                 :            : import {VersionModule} from "../../../modules/VersionModule.sol";
+      13                 :            : import {
+      14                 :            :     RuleConditionalTransferLightMultiTokenInvariantStorage
+      15                 :            : } from "./RuleConditionalTransferLightMultiTokenInvariantStorage.sol";
+      16                 :            : import {ITransferContext} from "../../interfaces/ITransferContext.sol";
+      17                 :            : 
+      18                 :            : /**
+      19                 :            :  * @title RuleConditionalTransferLightMultiTokenBase — conditional-transfer rule wiring per-token approval state into the compliance interfaces
+      20                 :            :  */
+      21                 :            : abstract contract RuleConditionalTransferLightMultiTokenBase is
+      22                 :            :     VersionModule,
+      23                 :            :     ERC3643ComplianceModule,
+      24                 :            :     RuleConditionalTransferLightMultiTokenInvariantStorage,
+      25                 :            :     IRule
+      26                 :            : {
+      27                 :            :     using SafeERC20 for IERC20;
+      28                 :            : 
+      29                 :            :     /**
+      30                 :            :      * @notice Number of outstanding approvals for each per-token transfer hash
+      31                 :            :      */
+      32                 :            :     mapping(bytes32 => uint256) public approvalCounts;
+      33                 :            : 
+      34                 :         24 :     modifier onlyTransferApprover() {
+      35                 :         24 :         _authorizeTransferApproval();
+      36                 :            :         _;
+      37                 :            :     }
+      38                 :            : 
+      39                 :          3 :     modifier onlyTransferExecutor() {
+      40                 :          3 :         _authorizeTransferExecution();
+      41                 :            :         _;
+      42                 :            :     }
+      43                 :            : 
+      44                 :            :     /**
+      45                 :            :      * @notice Compliance hook invoked when tokens are created (minted); consumes an approval if applicable.
+      46                 :            :      * @param to The recipient of the created tokens.
+      47                 :            :      * @param value The amount of tokens created.
+      48                 :            :      */
+      49                 :          2 :     function created(address to, uint256 value) external onlyBoundToken {
+      50                 :          1 :         _transferred(_msgSender(), address(0), to, value);
+      51                 :            :     }
+      52                 :            : 
+      53                 :            :     /**
+      54                 :            :      * @notice Compliance hook invoked when tokens are destroyed (burned); consumes an approval if applicable.
+      55                 :            :      * @param from The holder whose tokens are destroyed.
+      56                 :            :      * @param value The amount of tokens destroyed.
+      57                 :            :      */
+      58                 :          2 :     function destroyed(address from, uint256 value) external onlyBoundToken {
+      59                 :          1 :         _transferred(_msgSender(), from, address(0), value);
+      60                 :            :     }
+      61                 :            : 
+      62                 :            :     /**
+      63                 :            :      * @notice Consumes one approval for the transfer described by `ctx`, using the caller as the token.
+      64                 :            :      * @param ctx The fungible transfer context (from, to, value).
+      65                 :            :      */
+      66                 :          3 :     function transferred(ITransferContext.FungibleTransferContext calldata ctx) external onlyTransferExecutor {
+      67                 :          3 :         _transferred(_msgSender(), ctx.from, ctx.to, ctx.value);
+      68                 :            :     }
+      69                 :            : 
+      70                 :            :     /**
+      71                 :            :      * @inheritdoc IRule
+      72                 :            :      */
+      73                 :          2 :     function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) {
+      74                 :          2 :         return restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED;
+      75                 :            :     }
+      76                 :            : 
+      77                 :            :     /**
+      78                 :            :      * @inheritdoc IERC1404
+      79                 :            :      */
+      80                 :          2 :     function messageForTransferRestriction(uint8 restrictionCode)
+      81                 :            :         external
+      82                 :            :         pure
+      83                 :            :         override(IERC1404)
+      84                 :            :         returns (string memory)
+      85                 :            :     {
+      86            [ + ]:          2 :         if (restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED) {
+      87                 :          1 :             return TEXT_TRANSFER_REQUEST_NOT_APPROVED;
+      88                 :            :         }
+      89                 :          1 :         return TEXT_CODE_NOT_FOUND;
+      90                 :            :     }
+      91                 :            : 
+      92                 :            :     /**
+      93                 :            :      * @notice Records a new approval for the given per-token transfer.
+      94                 :            :      * @param token The token the transfer applies to.
+      95                 :            :      * @param from The sender of the transfer to approve.
+      96                 :            :      * @param to The recipient of the transfer to approve.
+      97                 :            :      * @param value The amount of the transfer to approve.
+      98                 :            :      */
+      99                 :         24 :     function approveTransfer(address token, address from, address to, uint256 value) public onlyTransferApprover {
+     100                 :         24 :         _approveTransfer(token, from, to, value);
+     101                 :            :     }
+     102                 :            : 
+     103                 :            :     /**
+     104                 :            :      * @notice Cancels one outstanding approval for the given per-token transfer.
+     105                 :            :      * @param token The token the transfer applies to.
+     106                 :            :      * @param from The sender of the transfer whose approval is cancelled.
+     107                 :            :      * @param to The recipient of the transfer whose approval is cancelled.
+     108                 :            :      * @param value The amount of the transfer whose approval is cancelled.
+     109                 :            :      */
+     110                 :          3 :     function cancelTransferApproval(address token, address from, address to, uint256 value)
+     111                 :            :         public
+     112                 :            :         onlyTransferApprover
+     113                 :            :     {
+     114                 :          2 :         _cancelTransferApproval(token, from, to, value);
+     115                 :            :     }
+     116                 :            : 
+     117                 :            :     /**
+     118                 :            :      * @notice Approves and performs a transferFrom of `token` using this rule as spender.
+     119                 :            :      * @dev Requires `from` to have approved this contract on `token`; the token must be bound.
+     120                 :            :      * @param token The token to transfer.
+     121                 :            :      * @param from The holder to transfer tokens from.
+     122                 :            :      * @param to The recipient of the transfer.
+     123                 :            :      * @param value The amount to transfer.
+     124                 :            :      * @return True when the transfer succeeds.
+     125                 :            :      */
+     126                 :          1 :     function approveAndTransferIfAllowed(address token, address from, address to, uint256 value)
+     127                 :            :         public
+     128                 :            :         onlyTransferApprover
+     129                 :            :         returns (bool)
+     130                 :            :     {
+     131         [ #  + ]:          1 :         require(isTokenBound(token), RuleConditionalTransferLightMultiToken_InvalidToken());
+     132                 :            : 
+     133                 :          1 :         _approveTransfer(token, from, to, value);
+     134                 :            : 
+     135                 :          1 :         uint256 allowed = IERC20(token).allowance(from, address(this));
+     136         [ #  + ]:          1 :         require(
+     137                 :            :             allowed >= value, RuleConditionalTransferLightMultiToken_InsufficientAllowance(token, from, allowed, value)
+     138                 :            :         );
+     139                 :            : 
+     140                 :          1 :         IERC20(token).safeTransferFrom(from, to, value);
+     141                 :          1 :         return true;
+     142                 :            :     }
+     143                 :            : 
+     144                 :            :     /**
+     145                 :            :      * @inheritdoc IERC3643IComplianceContract
+     146                 :            :      */
+     147                 :          4 :     function transferred(address from, address to, uint256 value)
+     148                 :            :         public
+     149                 :            :         override(IERC3643IComplianceContract)
+     150                 :            :         onlyTransferExecutor
+     151                 :            :     {
+     152                 :          4 :         _transferred(_msgSender(), from, to, value);
+     153                 :            :     }
+     154                 :            : 
+     155                 :            :     /**
+     156                 :            :      * @inheritdoc IRuleEngine
+     157                 :            :      */
+     158                 :          6 :     function transferred(
+     159                 :            :         address,
+     160                 :            :         /* spender */
+     161                 :            :         address from,
+     162                 :            :         address to,
+     163                 :            :         uint256 value
+     164                 :            :     )
+     165                 :            :         public
+     166                 :            :         override(IRuleEngine)
+     167                 :            :         onlyTransferExecutor
+     168                 :            :     {
+     169                 :          6 :         _transferred(_msgSender(), from, to, value);
+     170                 :            :     }
+     171                 :            : 
+     172                 :            :     /**
+     173                 :            :      * @notice Discards every outstanding approval for the given per-token transfer in one call.
+     174                 :            :      * @dev
+     175                 :            :      * - Reverts if no approval exists, per the single-item convention (use {cancelTransferApproval}
+     176                 :            :      *   to remove exactly one).
+     177                 :            :      * - Deliberately does NOT require the token to be bound, unlike {approveTransfer}: the primary
+     178                 :            :      *   use is cleaning up approvals that survived an {unbindToken}, at which point the token is by
+     179                 :            :      *   definition no longer bound. It is also the only way to clear approvals stranded under a key
+     180                 :            :      *   that can never be consumed (see `RESULT.md` finding F-4).
+     181                 :            :      * @param token The token whose approvals are cleared.
+     182                 :            :      * @param from The sender of the transfer whose approvals are cleared.
+     183                 :            :      * @param to The recipient of the transfer whose approvals are cleared.
+     184                 :            :      * @param value The amount of the transfer whose approvals are cleared.
+     185                 :            :      * @return cleared The approval count that was discarded.
+     186                 :            :      */
+     187                 :          3 :     function resetApproval(address token, address from, address to, uint256 value)
+     188                 :            :         public
+     189                 :            :         virtual
+     190                 :            :         onlyTransferApprover
+     191                 :            :         returns (uint256 cleared)
+     192                 :            :     {
+     193                 :          2 :         bytes32 transferHash = _transferHash(token, from, to, value);
+     194                 :          2 :         cleared = approvalCounts[transferHash];
+     195         [ +  + ]:          2 :         require(cleared != 0, RuleConditionalTransferLightMultiToken_TransferApprovalNotFound());
+     196                 :          1 :         approvalCounts[transferHash] = 0;
+     197                 :          1 :         emit TransferApprovalReset(token, from, to, value, cleared);
+     198                 :            :     }
+     199                 :            : 
+     200                 :            :     /**
+     201                 :            :      * @notice Returns the number of outstanding approvals for the given per-token transfer.
+     202                 :            :      * @param token The token the transfer applies to.
+     203                 :            :      * @param from The sender of the transfer.
+     204                 :            :      * @param to The recipient of the transfer.
+     205                 :            :      * @param value The amount of the transfer.
+     206                 :            :      * @return The current approval count for the transfer.
+     207                 :            :      */
+     208                 :         16 :     function approvedCount(address token, address from, address to, uint256 value) public view returns (uint256) {
+     209                 :         16 :         bytes32 transferHash = _transferHash(token, from, to, value);
+     210                 :         16 :         return approvalCounts[transferHash];
+     211                 :            :     }
+     212                 :            : 
+     213                 :            :     /**
+     214                 :            :      * @inheritdoc IERC1404
+     215                 :            :      * @dev CALLER-DEPENDENT. The token key is derived from `msg.sender`, so this view only returns a
+     216                 :            :      *      meaningful answer when it is invoked BY the bound token. Any other caller — an off-chain
+     217                 :            :      *      `eth_call` from a wallet, an explorer, an aggregator — always receives
+     218                 :            :      *      `CODE_TRANSFER_REQUEST_NOT_APPROVED`, even for a transfer that is approved and will succeed.
+     219                 :            :      *      It is fail-closed, but it carries no signal for third-party pre-flight.
+     220                 :            :      *      Use {detectTransferRestrictionForToken} instead, which takes the token explicitly.
+     221                 :            :      */
+     222                 :        265 :     function detectTransferRestriction(address from, address to, uint256 value)
+     223                 :            :         public
+     224                 :            :         view
+     225                 :            :         override(IERC1404)
+     226                 :            :         returns (uint8)
+     227                 :            :     {
+     228                 :        270 :         return _detectTransferRestrictionForToken(_msgSender(), from, to, value);
+     229                 :            :     }
+     230                 :            : 
+     231                 :            :     /**
+     232                 :            :      * @notice Caller-explicit pre-flight: returns the restriction code for a transfer of `token`.
+     233                 :            :      * @dev Unlike {detectTransferRestriction}, the token is passed in rather than derived from
+     234                 :            :      *      `msg.sender`, so any caller (notably an off-chain `eth_call`) gets the real answer.
+     235                 :            :      *      This is the view integrators should use.
+     236                 :            :      * @param token The token the transfer applies to.
+     237                 :            :      * @param from The sender of the transfer.
+     238                 :            :      * @param to The recipient of the transfer.
+     239                 :            :      * @param value The amount of the transfer.
+     240                 :            :      * @return The restriction code, or TRANSFER_OK when an approval exists.
+     241                 :            :      */
+     242                 :        263 :     function detectTransferRestrictionForToken(address token, address from, address to, uint256 value)
+     243                 :            :         public
+     244                 :            :         view
+     245                 :            :         virtual
+     246                 :            :         returns (uint8)
+     247                 :            :     {
+     248                 :        263 :         return _detectTransferRestrictionForToken(token, from, to, value);
+     249                 :            :     }
+     250                 :            : 
+     251                 :            :     /**
+     252                 :            :      * @notice Caller-explicit pre-flight: whether a transfer of `token` is currently approved.
+     253                 :            :      * @dev The boolean counterpart of {detectTransferRestrictionForToken}. Prefer this over
+     254                 :            :      *      {canTransfer}, which is caller-dependent.
+     255                 :            :      * @param token The token the transfer applies to.
+     256                 :            :      * @param from The sender of the transfer.
+     257                 :            :      * @param to The recipient of the transfer.
+     258                 :            :      * @param value The amount of the transfer.
+     259                 :            :      * @return True when the transfer is approved for `token`.
+     260                 :            :      */
+     261                 :          5 :     function canTransferForToken(address token, address from, address to, uint256 value)
+     262                 :            :         public
+     263                 :            :         view
+     264                 :            :         virtual
+     265                 :            :         returns (bool)
+     266                 :            :     {
+     267                 :          5 :         return _detectTransferRestrictionForToken(token, from, to, value)
+     268                 :            :             == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+     269                 :            :     }
+     270                 :            : 
+     271                 :            :     /**
+     272                 :            :      * @inheritdoc IERC1404Extend
+     273                 :            :      */
+     274                 :          2 :     function detectTransferRestrictionFrom(
+     275                 :            :         address,
+     276                 :            :         /* spender */
+     277                 :            :         address from,
+     278                 :            :         address to,
+     279                 :            :         uint256 value
+     280                 :            :     )
+     281                 :            :         public
+     282                 :            :         view
+     283                 :            :         override(IERC1404Extend)
+     284                 :            :         returns (uint8)
+     285                 :            :     {
+     286                 :          4 :         return detectTransferRestriction(from, to, value);
+     287                 :            :     }
+     288                 :            : 
+     289                 :            :     /**
+     290                 :            :      * @inheritdoc IERC3643ComplianceRead
+     291                 :            :      * @dev CALLER-DEPENDENT, for the same reason as {detectTransferRestriction}: a caller that is not
+     292                 :            :      *      the bound token always reads `false`. Use {canTransferForToken} for an off-chain pre-flight.
+     293                 :            :      */
+     294                 :          1 :     function canTransfer(address from, address to, uint256 value)
+     295                 :            :         public
+     296                 :            :         view
+     297                 :            :         override(IERC3643ComplianceRead)
+     298                 :            :         returns (bool)
+     299                 :            :     {
+     300                 :          1 :         return detectTransferRestriction(from, to, value) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+     301                 :            :     }
+     302                 :            : 
+     303                 :            :     /**
+     304                 :            :      * @inheritdoc IERC7551Compliance
+     305                 :            :      */
+     306                 :          2 :     function canTransferFrom(address spender, address from, address to, uint256 value)
+     307                 :            :         public
+     308                 :            :         view
+     309                 :            :         override(IERC7551Compliance)
+     310                 :            :         returns (bool)
+     311                 :            :     {
+     312                 :          2 :         return detectTransferRestrictionFrom(spender, from, to, value)
+     313                 :            :             == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+     314                 :            :     }
+     315                 :            : 
+     316                 :            :     /**
+     317                 :            :      * @notice Computes the restriction code for a transfer of `token`, independently of the caller.
+     318                 :            :      * @dev Single source of truth for the read path: {detectTransferRestriction} feeds it
+     319                 :            :      *      `_msgSender()`, while {detectTransferRestrictionForToken} feeds it an explicit token, so
+     320                 :            :      *      the two can never disagree. Mints and burns are exempt; an unbound token has no
+     321                 :            :      *      consumable approvals and is therefore always restricted (fail-closed).
+     322                 :            :      * @param token The token the transfer applies to.
+     323                 :            :      * @param from The sender of the transfer.
+     324                 :            :      * @param to The recipient of the transfer.
+     325                 :            :      * @param value The amount of the transfer.
+     326                 :            :      * @return The restriction code, or TRANSFER_OK when an approval exists.
+     327                 :            :      */
+     328                 :        538 :     function _detectTransferRestrictionForToken(address token, address from, address to, uint256 value)
+     329                 :            :         internal
+     330                 :            :         view
+     331                 :            :         virtual
+     332                 :            :         returns (uint8)
+     333                 :            :     {
+     334            [ + ]:        538 :         if (from == address(0) || to == address(0)) {
+     335                 :          2 :             return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+     336                 :            :         }
+     337                 :            : 
+     338            [ + ]:        536 :         if (!isTokenBound(token)) {
+     339                 :          7 :             return CODE_TRANSFER_REQUEST_NOT_APPROVED;
+     340                 :            :         }
+     341                 :            : 
+     342            [ + ]:        529 :         if (approvalCounts[_transferHash(token, from, to, value)] == 0) {
+     343                 :        519 :             return CODE_TRANSFER_REQUEST_NOT_APPROVED;
+     344                 :            :         }
+     345                 :            : 
+     346                 :         10 :         return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+     347                 :            :     }
+     348                 :            : 
+     349                 :            :     /**
+     350                 :            :      * @notice Authorizes changes to compliance binding: restricted to the compliance manager.
+     351                 :            :      * @dev NOT `view`, unlike every other access-control hook in this codebase. This is structural,
+     352                 :            :      *      not an oversight: the implementation delegates to `_onlyComplianceManager()`, which
+     353                 :            :      *      `lib/RuleEngine`'s {ERC3643ComplianceModule} declares as `internal virtual` (non-`view`).
+     354                 :            :      *      Solidity checks mutability against a virtual's DECLARED type, not the installed override,
+     355                 :            :      *      so calling it from a `view` function is a compile error — even though every override of it
+     356                 :            :      *      in this repo is `view`. It can only become `view` once the upstream declaration does.
+     357                 :            :      *      (The single-token rules avoid this by overriding this hook directly with `onlyRole(...)`
+     358                 :            :      *      instead of delegating, which is why they are already `view`.)
+     359                 :            :      */
+     360                 :         38 :     function _authorizeComplianceBindingChange(address) internal virtual override {
+     361                 :         38 :         _onlyComplianceManager();
+     362                 :            :     }
+     363                 :            : 
+     364                 :            :     /**
+     365                 :            :      * @notice Records a new approval for the given per-token transfer; reverts if the token is not bound.
+     366                 :            :      * @param token The token the transfer applies to.
+     367                 :            :      * @param from The sender of the transfer.
+     368                 :            :      * @param to The recipient of the transfer.
+     369                 :            :      * @param value The amount of the transfer.
+     370                 :            :      */
+     371                 :         25 :     function _approveTransfer(address token, address from, address to, uint256 value) internal virtual {
+     372         [ +  + ]:         25 :         require(isTokenBound(token), RuleConditionalTransferLightMultiToken_InvalidToken());
+     373                 :         23 :         bytes32 transferHash = _transferHash(token, from, to, value);
+     374                 :         23 :         approvalCounts[transferHash] += 1;
+     375                 :         23 :         emit TransferApproved(token, from, to, value, approvalCounts[transferHash]);
+     376                 :            :     }
+     377                 :            : 
+     378                 :            :     /**
+     379                 :            :      * @notice Cancels one outstanding approval for the given per-token transfer; reverts if none exists.
+     380                 :            :      * @param token The token the transfer applies to.
+     381                 :            :      * @param from The sender of the transfer.
+     382                 :            :      * @param to The recipient of the transfer.
+     383                 :            :      * @param value The amount of the transfer.
+     384                 :            :      */
+     385                 :          2 :     function _cancelTransferApproval(address token, address from, address to, uint256 value) internal virtual {
+     386         [ #  + ]:          2 :         require(isTokenBound(token), RuleConditionalTransferLightMultiToken_InvalidToken());
+     387                 :          2 :         bytes32 transferHash = _transferHash(token, from, to, value);
+     388                 :          2 :         uint256 count = approvalCounts[transferHash];
+     389                 :            : 
+     390         [ +  + ]:          2 :         require(count != 0, RuleConditionalTransferLightMultiToken_TransferApprovalNotFound());
+     391                 :            : 
+     392                 :          1 :         approvalCounts[transferHash] = count - 1;
+     393                 :          1 :         emit TransferApprovalCancelled(token, from, to, value, approvalCounts[transferHash]);
+     394                 :            :     }
+     395                 :            : 
+     396                 :            :     /**
+     397                 :            :      * @notice Consumes one approval for the given per-token transfer; reverts if none exists.
+     398                 :            :      * @dev No-op when either endpoint is the zero address (mint/burn).
+     399                 :            :      * @param token The token the transfer applies to.
+     400                 :            :      * @param from The sender of the transfer.
+     401                 :            :      * @param to The recipient of the transfer.
+     402                 :            :      * @param value The amount of the transfer.
+     403                 :            :      */
+     404                 :         15 :     function _transferred(address token, address from, address to, uint256 value) internal virtual {
+     405            [ + ]:         15 :         if (from == address(0) || to == address(0)) {
+     406                 :         15 :             return;
+     407                 :            :         }
+     408                 :            : 
+     409                 :          9 :         bytes32 transferHash = _transferHash(token, from, to, value);
+     410                 :          9 :         uint256 count = approvalCounts[transferHash];
+     411                 :            : 
+     412         [ +  + ]:          9 :         require(count != 0, RuleConditionalTransferLightMultiToken_TransferNotApproved());
+     413                 :            : 
+     414                 :          6 :         approvalCounts[transferHash] = count - 1;
+     415                 :          6 :         emit TransferExecuted(token, from, to, value, approvalCounts[transferHash]);
+     416                 :            :     }
+     417                 :            : 
+     418                 :            :     /**
+     419                 :            :      * @notice Authorizes transfer execution: only a bound token may call the execution hooks.
+     420                 :            :      */
+     421                 :         13 :     function _authorizeTransferExecution() internal view virtual {
+     422         [ #  + ]:         13 :         require(
+     423                 :            :             isTokenBound(_msgSender()),
+     424                 :            :             RuleConditionalTransferLightMultiToken_TransferExecutorUnauthorized(_msgSender())
+     425                 :            :         );
+     426                 :            :     }
+     427                 :            : 
+     428                 :            :     /**
+     429                 :            :      * @notice Authorizes the caller to approve or cancel transfers; reverts if unauthorized.
+     430                 :            :      */
+     431                 :          0 :     function _authorizeTransferApproval() internal view virtual;
+     432                 :            : 
+     433                 :            :     /**
+     434                 :            :      * @notice Computes the storage key identifying a (token, from, to, value) transfer.
+     435                 :            :      * @param token The token the transfer applies to.
+     436                 :            :      * @param from The sender of the transfer.
+     437                 :            :      * @param to The recipient of the transfer.
+     438                 :            :      * @param value The amount of the transfer.
+     439                 :            :      * @return hash The keccak256 hash uniquely identifying the transfer.
+     440                 :            :      */
+     441                 :        581 :     function _transferHash(address token, address from, address to, uint256 value)
+     442                 :            :         internal
+     443                 :            :         pure
+     444                 :            :         virtual
+     445                 :            :         returns (bytes32 hash)
+     446                 :            :     {
+     447                 :            :         assembly ("memory-safe") {
+     448                 :        581 :             let ptr := mload(0x40)
+     449                 :        581 :             mstore(ptr, shl(96, token))
+     450                 :        581 :             mstore(add(ptr, 0x20), shl(96, from))
+     451                 :        581 :             mstore(add(ptr, 0x40), shl(96, to))
+     452                 :        581 :             mstore(add(ptr, 0x60), value)
+     453                 :        581 :             hash := keccak256(ptr, 0x80)
+     454                 :            :         }
+     455                 :            :     }
+     456                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/abstract/RuleMintAllowanceBase.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/operation/abstract/RuleMintAllowanceBase.sol.func-sort-c.html new file mode 100644 index 0000000..4dfabb5 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/abstract/RuleMintAllowanceBase.sol.func-sort-c.html @@ -0,0 +1,165 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/abstract/RuleMintAllowanceBase.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation/abstract - RuleMintAllowanceBase.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:565798.2 %
Date:2026-07-14 13:44:06Functions:202195.2 %
Branches:99100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleMintAllowanceBase._authorizeSetMintAllowance0
RuleMintAllowanceBase.created1
RuleMintAllowanceBase.destroyed1
RuleMintAllowanceBase._transferred2
RuleMintAllowanceBase.canReturnTransferRestrictionCode2
RuleMintAllowanceBase.messageForTransferRestriction2
RuleMintAllowanceBase.canTransfer3
RuleMintAllowanceBase.transferred.03
RuleMintAllowanceBase.clearMintAllowances4
RuleMintAllowanceBase.onlyAllowanceOperator4
RuleMintAllowanceBase.detectTransferRestriction5
RuleMintAllowanceBase.canTransferFrom6
RuleMintAllowanceBase.detectTransferRestrictionFrom9
RuleMintAllowanceBase._detectTransferRestrictionFrom15
RuleMintAllowanceBase.bindToken323
RuleMintAllowanceBase.decreaseMintAllowance3078
RuleMintAllowanceBase.increaseMintAllowance3241
RuleMintAllowanceBase.setMintAllowance3753
RuleMintAllowanceBase._setMintAllowance3758
RuleMintAllowanceBase._transferredFrom6848
RuleMintAllowanceBase.transferred.16849
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/abstract/RuleMintAllowanceBase.sol.func.html b/doc/coverage/coverage/src/rules/operation/abstract/RuleMintAllowanceBase.sol.func.html new file mode 100644 index 0000000..867ea8c --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/abstract/RuleMintAllowanceBase.sol.func.html @@ -0,0 +1,165 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/abstract/RuleMintAllowanceBase.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation/abstract - RuleMintAllowanceBase.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:565798.2 %
Date:2026-07-14 13:44:06Functions:202195.2 %
Branches:99100.0 %
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by hit count
RuleMintAllowanceBase._authorizeSetMintAllowance0
RuleMintAllowanceBase._detectTransferRestrictionFrom15
RuleMintAllowanceBase._setMintAllowance3758
RuleMintAllowanceBase._transferred2
RuleMintAllowanceBase._transferredFrom6848
RuleMintAllowanceBase.bindToken323
RuleMintAllowanceBase.canReturnTransferRestrictionCode2
RuleMintAllowanceBase.canTransfer3
RuleMintAllowanceBase.canTransferFrom6
RuleMintAllowanceBase.clearMintAllowances4
RuleMintAllowanceBase.created1
RuleMintAllowanceBase.decreaseMintAllowance3078
RuleMintAllowanceBase.destroyed1
RuleMintAllowanceBase.detectTransferRestriction5
RuleMintAllowanceBase.detectTransferRestrictionFrom9
RuleMintAllowanceBase.increaseMintAllowance3241
RuleMintAllowanceBase.messageForTransferRestriction2
RuleMintAllowanceBase.onlyAllowanceOperator4
RuleMintAllowanceBase.setMintAllowance3753
RuleMintAllowanceBase.transferred.03
RuleMintAllowanceBase.transferred.16849
+
+
+ + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/abstract/RuleMintAllowanceBase.sol.gcov.html b/doc/coverage/coverage/src/rules/operation/abstract/RuleMintAllowanceBase.sol.gcov.html new file mode 100644 index 0000000..6ec75d5 --- /dev/null +++ b/doc/coverage/coverage/src/rules/operation/abstract/RuleMintAllowanceBase.sol.gcov.html @@ -0,0 +1,400 @@ + + + + + + + LCOV - lcov.info - src/rules/operation/abstract/RuleMintAllowanceBase.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rules/operation/abstract - RuleMintAllowanceBase.sol (source / functions)HitTotalCoverage
Test:lcov.infoLines:565798.2 %
Date:2026-07-14 13:44:06Functions:202195.2 %
Branches:99100.0 %
+
+ + + + + + + + +

+
           Branch data     Line data    Source code
+
+       1                 :            : // SPDX-License-Identifier: MPL-2.0
+       2                 :            : pragma solidity ^0.8.20;
+       3                 :            : 
+       4                 :            : import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol";
+       5                 :            : import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol";
+       6                 :            : import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol";
+       7                 :            : import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol";
+       8                 :            : import {IRule} from "RuleEngine/interfaces/IRule.sol";
+       9                 :            : import {ERC3643ComplianceModule} from "RuleEngine/modules/ERC3643ComplianceModule.sol";
+      10                 :            : import {VersionModule} from "../../../modules/VersionModule.sol";
+      11                 :            : import {RuleMintAllowanceInvariantStorage} from "./RuleMintAllowanceInvariantStorage.sol";
+      12                 :            : 
+      13                 :            : /**
+      14                 :            :  * @title RuleMintAllowanceBase
+      15                 :            :  * @notice Core logic for per-minter mint quota enforcement.
+      16                 :            :  * @dev Operators set the number of tokens each minter address is allowed to mint.
+      17                 :            :  *      Each mint reduces the minter's allowance. Allowances can be set to an absolute
+      18                 :            :  *      value or adjusted incrementally via `increaseMintAllowance`/`decreaseMintAllowance`.
+      19                 :            :  *
+      20                 :            :  *      The rule tracks mints via the 4-arg `transferred(spender, from=0, to, value)` path
+      21                 :            :  *      introduced in CMTAT v3.3. The 3-arg `transferred(from=0, to, value)` path has no
+      22                 :            :  *      minter identity and performs no deduction.
+      23                 :            :  *
+      24                 :            :  *      `detectTransferRestriction(from, to, value)` always returns TRANSFER_OK because
+      25                 :            :  *      the minter identity is unavailable in the 3-arg call; use
+      26                 :            :  *      `detectTransferRestrictionFrom(minter, address(0), to, amount)` to query allowance.
+      27                 :            :  *
+      28                 :            :  *      Callers of the state-modifying `transferred()` functions must be bound via
+      29                 :            :  *      `bindToken(ruleEngineAddress)` before minting starts. In a standard CMTAT +
+      30                 :            :  *      RuleEngine setup the RuleEngine address is the entity to bind.
+      31                 :            :  */
+      32                 :            : abstract contract RuleMintAllowanceBase is
+      33                 :            :     VersionModule,
+      34                 :            :     ERC3643ComplianceModule,
+      35                 :            :     RuleMintAllowanceInvariantStorage,
+      36                 :            :     IRule
+      37                 :            : {
+      38                 :            :     /*//////////////////////////////////////////////////////////////
+      39                 :            :                              STATE
+      40                 :            :     //////////////////////////////////////////////////////////////*/
+      41                 :            : 
+      42                 :            :     /**
+      43                 :            :      * @notice Remaining mint allowance for each minter address, in token base units
+      44                 :            :      */
+      45                 :            :     mapping(address minter => uint256 allowance) public mintAllowance;
+      46                 :            : 
+      47                 :            :     /*//////////////////////////////////////////////////////////////
+      48                 :            :                         ACCESS CONTROL
+      49                 :            :     //////////////////////////////////////////////////////////////*/
+      50                 :            : 
+      51                 :          4 :     modifier onlyAllowanceOperator() {
+      52                 :          4 :         _authorizeSetMintAllowance();
+      53                 :            :         _;
+      54                 :            :     }
+      55                 :            : 
+      56                 :            :     /*//////////////////////////////////////////////////////////////
+      57                 :            :                         EXTERNAL FUNCTIONS
+      58                 :            :     //////////////////////////////////////////////////////////////*/
+      59                 :            : 
+      60                 :            :     /**
+      61                 :            :      * @notice Compliance hook invoked when tokens are created (minted); no-op for this rule.
+      62                 :            :      */
+      63                 :          1 :     function created(address, uint256) external virtual override onlyBoundToken {}
+      64                 :            : 
+      65                 :            :     /**
+      66                 :            :      * @notice Compliance hook invoked when tokens are destroyed (burned); no-op for this rule.
+      67                 :            :      */
+      68                 :          1 :     function destroyed(address, uint256) external virtual override onlyBoundToken {}
+      69                 :            : 
+      70                 :            :     /**
+      71                 :            :      * @inheritdoc IRule
+      72                 :            :      */
+      73                 :          2 :     function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) {
+      74                 :          2 :         return restrictionCode == CODE_MINTER_ALLOWANCE_EXCEEDED;
+      75                 :            :     }
+      76                 :            : 
+      77                 :            :     /*//////////////////////////////////////////////////////////////
+      78                 :            :                         PUBLIC FUNCTIONS
+      79                 :            :     //////////////////////////////////////////////////////////////*/
+      80                 :            : 
+      81                 :            :     /**
+      82                 :            :      * @notice Sets `minter`'s allowance to an absolute `amount`.
+      83                 :            :      * @param minter The minter whose allowance is set.
+      84                 :            :      * @param amount The absolute allowance to assign.
+      85                 :            :      */
+      86                 :       3753 :     function setMintAllowance(address minter, uint256 amount) public virtual onlyAllowanceOperator {
+      87                 :       3751 :         _setMintAllowance(minter, amount);
+      88                 :            :     }
+      89                 :            : 
+      90                 :            :     /**
+      91                 :            :      * @notice Increases `minter`'s allowance by `amount`.
+      92                 :            :      * @param minter The minter whose allowance is increased.
+      93                 :            :      * @param amount The amount to add to the allowance.
+      94                 :            :      */
+      95                 :       3241 :     function increaseMintAllowance(address minter, uint256 amount) public virtual onlyAllowanceOperator {
+      96                 :       3240 :         uint256 newAllowance = mintAllowance[minter] + amount;
+      97                 :       3240 :         mintAllowance[minter] = newAllowance;
+      98                 :       3240 :         emit MintAllowanceIncreased(minter, amount, newAllowance);
+      99                 :            :     }
+     100                 :            : 
+     101                 :            :     /**
+     102                 :            :      * @notice Decreases `minter`'s allowance by `amount`. Reverts if the reduction
+     103                 :            :      *         would underflow (i.e. `amount > current allowance`).
+     104                 :            :      * @param minter The minter whose allowance is decreased.
+     105                 :            :      * @param amount The amount to subtract from the allowance.
+     106                 :            :      */
+     107                 :       3078 :     function decreaseMintAllowance(address minter, uint256 amount) public virtual onlyAllowanceOperator {
+     108                 :       3077 :         uint256 current = mintAllowance[minter];
+     109         [ +  + ]:       3077 :         require(amount <= current, RuleMintAllowance_DecreaseBelowZero(minter, current, amount));
+     110                 :       3076 :         uint256 newAllowance = current - amount;
+     111                 :       3076 :         mintAllowance[minter] = newAllowance;
+     112                 :       3076 :         emit MintAllowanceDecreased(minter, amount, newAllowance);
+     113                 :            :     }
+     114                 :            : 
+     115                 :            :     /**
+     116                 :            :      * @notice Sets the allowance of every listed minter to zero.
+     117                 :            :      * @dev
+     118                 :            :      *      Batch operation: does not revert on minters that already have a zero allowance.
+     119                 :            :      *      Intended for migration: `unbindToken` does NOT clear `mintAllowance`, so call this
+     120                 :            :      *      before rebinding to a different RuleEngine/token if the previous quotas must not
+     121                 :            :      *      carry over. See the {bindToken} warning.
+     122                 :            :      * @param minters The minters whose allowances are cleared.
+     123                 :            :      */
+     124                 :          4 :     function clearMintAllowances(address[] calldata minters) public virtual onlyAllowanceOperator {
+     125                 :          3 :         for (uint256 i = 0; i < minters.length; ++i) {
+     126                 :          7 :             _setMintAllowance(minters[i], 0);
+     127                 :            :         }
+     128                 :            :     }
+     129                 :            : 
+     130                 :            :     /**
+     131                 :            :      * @notice Binds a caller to this rule. Reverts if a caller is already bound.
+     132                 :            :      * @dev Enforces single-target binding to prevent one allowance state from being
+     133                 :            :      *      shared across several RuleEngines/tokens. To migrate, call `unbindToken`
+     134                 :            :      *      first, then bind the new caller.
+     135                 :            :      * @dev WARNING: `unbindToken` does not clear `mintAllowance`. Quotas granted while the
+     136                 :            :      *      previous RuleEngine/token was bound remain in storage and are spendable by the same
+     137                 :            :      *      minters once a new caller is bound. The operator who controls rebinding also controls
+     138                 :            :      *      allowances, so the trust model is preserved, but integrators should be aware of this
+     139                 :            :      *      behavior. Call {clearMintAllowances} before rebinding to discard the previous quotas.
+     140                 :            :      * @param token The caller (RuleEngine/token) to bind to this rule.
+     141                 :            :      */
+     142                 :        323 :     function bindToken(address token) public virtual override onlyComplianceManager {
+     143         [ +  + ]:        321 :         require(getTokenBound() == address(0), RuleMintAllowance_TokenAlreadyBound());
+     144                 :        319 :         _bindToken(token);
+     145                 :            :     }
+     146                 :            : 
+     147                 :            :     /**
+     148                 :            :      * @dev 3-arg path: no minter identity available; performs no deduction.
+     149                 :            :      *      Mints always arrive via the 4-arg path in CMTAT v3.3+.
+     150                 :            :      * @param from The sender address (address(0) for mints).
+     151                 :            :      * @param to The recipient address.
+     152                 :            :      * @param value The amount transferred.
+     153                 :            :      */
+     154                 :          3 :     function transferred(address from, address to, uint256 value)
+     155                 :            :         public
+     156                 :            :         virtual
+     157                 :            :         override(IERC3643IComplianceContract)
+     158                 :            :         onlyBoundToken
+     159                 :            :     {
+     160                 :          2 :         _transferred(from, to, value);
+     161                 :            :     }
+     162                 :            : 
+     163                 :            :     /**
+     164                 :            :      * @dev 4-arg path: `spender` is the minter when `from == address(0)`.
+     165                 :            :      *      Deducts `value` from `mintAllowance[spender]`; reverts if insufficient.
+     166                 :            :      * @param spender The minter identity when `from == address(0)`.
+     167                 :            :      * @param from The sender address (address(0) for mints).
+     168                 :            :      * @param to The recipient address.
+     169                 :            :      * @param value The amount transferred.
+     170                 :            :      */
+     171                 :       6849 :     function transferred(address spender, address from, address to, uint256 value)
+     172                 :            :         public
+     173                 :            :         virtual
+     174                 :            :         override(IRuleEngine)
+     175                 :            :         onlyBoundToken
+     176                 :            :     {
+     177                 :       6848 :         _transferredFrom(spender, from, to, value);
+     178                 :            :     }
+     179                 :            : 
+     180                 :            :     /**
+     181                 :            :      * @inheritdoc IERC1404
+     182                 :            :      */
+     183                 :          2 :     function messageForTransferRestriction(uint8 restrictionCode)
+     184                 :            :         public
+     185                 :            :         pure
+     186                 :            :         override(IERC1404)
+     187                 :            :         returns (string memory)
+     188                 :            :     {
+     189            [ + ]:          2 :         if (restrictionCode == CODE_MINTER_ALLOWANCE_EXCEEDED) {
+     190                 :          1 :             return TEXT_MINTER_ALLOWANCE_EXCEEDED;
+     191                 :            :         }
+     192                 :          1 :         return TEXT_CODE_NOT_FOUND;
+     193                 :            :     }
+     194                 :            : 
+     195                 :            :     /**
+     196                 :            :      * @dev Always returns TRANSFER_OK: the minter address is not available in the
+     197                 :            :      *      3-arg call. Call `detectTransferRestrictionFrom` to check a minter's quota.
+     198                 :            :      * @return The restriction code, always TRANSFER_OK.
+     199                 :            :      */
+     200                 :          5 :     function detectTransferRestriction(address, address, uint256)
+     201                 :            :         public
+     202                 :            :         view
+     203                 :            :         virtual
+     204                 :            :         override(IERC1404)
+     205                 :            :         returns (uint8)
+     206                 :            :     {
+     207                 :          5 :         return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+     208                 :            :     }
+     209                 :            : 
+     210                 :            :     /**
+     211                 :            :      * @inheritdoc IERC1404Extend
+     212                 :            :      */
+     213                 :          9 :     function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value)
+     214                 :            :         public
+     215                 :            :         view
+     216                 :            :         virtual
+     217                 :            :         override(IERC1404Extend)
+     218                 :            :         returns (uint8)
+     219                 :            :     {
+     220                 :         15 :         return _detectTransferRestrictionFrom(spender, from, to, value);
+     221                 :            :     }
+     222                 :            : 
+     223                 :            :     /**
+     224                 :            :      * @dev Always returns true: use `canTransferFrom` to check mint allowance.
+     225                 :            :      * @return Always true.
+     226                 :            :      */
+     227                 :          3 :     function canTransfer(address, address, uint256)
+     228                 :            :         public
+     229                 :            :         view
+     230                 :            :         virtual
+     231                 :            :         override(IERC3643ComplianceRead)
+     232                 :            :         returns (bool)
+     233                 :            :     {
+     234                 :          3 :         return true;
+     235                 :            :     }
+     236                 :            : 
+     237                 :            :     /**
+     238                 :            :      * @inheritdoc IERC7551Compliance
+     239                 :            :      */
+     240                 :          6 :     function canTransferFrom(address spender, address from, address to, uint256 value)
+     241                 :            :         public
+     242                 :            :         view
+     243                 :            :         virtual
+     244                 :            :         override(IERC7551Compliance)
+     245                 :            :         returns (bool)
+     246                 :            :     {
+     247                 :          6 :         return detectTransferRestrictionFrom(spender, from, to, value)
+     248                 :            :             == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+     249                 :            :     }
+     250                 :            : 
+     251                 :            :     /*//////////////////////////////////////////////////////////////
+     252                 :            :                         INTERNAL FUNCTIONS
+     253                 :            :     //////////////////////////////////////////////////////////////*/
+     254                 :            : 
+     255                 :            :     /**
+     256                 :            :      * @notice 3-arg path helper: regular transfers carry no minter identity and are not tracked.
+     257                 :            :      */
+     258                 :          2 :     function _transferred(address, address, uint256) internal virtual {
+     259                 :            :         // 3-arg path: no minter identity; regular transfers are not tracked by this rule.
+     260                 :            :     }
+     261                 :            : 
+     262                 :            :     /**
+     263                 :            :      * @notice Deducts a mint from the minter's allowance on the 4-arg path.
+     264                 :            :      * @dev No-op unless `from == address(0)` (a mint). Reverts if `value` exceeds
+     265                 :            :      *      the minter's remaining allowance.
+     266                 :            :      * @param spender The minter identity.
+     267                 :            :      * @param from The sender address (must be address(0) to deduct).
+     268                 :            :      * @param value The amount minted.
+     269                 :            :      */
+     270                 :       6848 :     function _transferredFrom(address spender, address from, address, uint256 value) internal virtual {
+     271            [ + ]:       6848 :         if (from != address(0)) {
+     272                 :       6848 :             return;
+     273                 :            :         }
+     274                 :       3577 :         uint256 current = mintAllowance[spender];
+     275         [ +  + ]:       3577 :         require(value <= current, RuleMintAllowance_AllowanceExceeded(address(this), spender, current, value));
+     276                 :       3325 :         uint256 remaining = current - value;
+     277                 :       3325 :         mintAllowance[spender] = remaining;
+     278                 :       3325 :         emit MintAllowanceConsumed(spender, value, remaining);
+     279                 :            :     }
+     280                 :            : 
+     281                 :            :     /**
+     282                 :            :      * @notice Sets a minter's allowance to an absolute value and emits the event.
+     283                 :            :      * @param minter The minter whose allowance is set.
+     284                 :            :      * @param amount The absolute allowance to assign.
+     285                 :            :      */
+     286                 :       3758 :     function _setMintAllowance(address minter, uint256 amount) internal virtual {
+     287                 :       3758 :         mintAllowance[minter] = amount;
+     288                 :       3758 :         emit MintAllowanceSet(minter, amount);
+     289                 :            :     }
+     290                 :            : 
+     291                 :            :     /**
+     292                 :            :      * @notice Computes whether a prospective mint would exceed the minter's allowance.
+     293                 :            :      * @dev Only mints (`from == address(0)`) are checked; other transfers return TRANSFER_OK.
+     294                 :            :      * @param spender The minter identity.
+     295                 :            :      * @param from The sender address (checked only when address(0)).
+     296                 :            :      * @param value The amount to be minted.
+     297                 :            :      * @return The restriction code (CODE_MINTER_ALLOWANCE_EXCEEDED or TRANSFER_OK).
+     298                 :            :      */
+     299                 :         15 :     function _detectTransferRestrictionFrom(address spender, address from, address, uint256 value)
+     300                 :            :         internal
+     301                 :            :         view
+     302                 :            :         virtual
+     303                 :            :         returns (uint8)
+     304                 :            :     {
+     305            [ + ]:         15 :         if (from == address(0) && mintAllowance[spender] < value) {
+     306                 :          7 :             return CODE_MINTER_ALLOWANCE_EXCEEDED;
+     307                 :            :         }
+     308                 :          8 :         return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK);
+     309                 :            :     }
+     310                 :            : 
+     311                 :            :     /**
+     312                 :            :      * @notice Authorizes the caller to set/adjust mint allowances; reverts if unauthorized.
+     313                 :            :      */
+     314                 :          0 :     function _authorizeSetMintAllowance() internal view virtual;
+     315                 :            : }
+
+
+
+ + + + +
Generated by: LCOV version 1.16
+
+ + + diff --git a/doc/coverage/coverage/src/rules/operation/abstract/index-sort-b.html b/doc/coverage/coverage/src/rules/operation/abstract/index-sort-b.html index b4ec123..cfa13ea 100644 --- a/doc/coverage/coverage/src/rules/operation/abstract/index-sort-b.html +++ b/doc/coverage/coverage/src/rules/operation/abstract/index-sort-b.html @@ -31,27 +31,27 @@ lcov.info Lines: - 75 - 77 - 97.4 % + 240 + 244 + 98.4 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 22 - 24 - 91.7 % + 73 + 77 + 94.8 % Branches: - 16 - 16 - 100.0 % + 50 + 54 + 92.6 % @@ -81,17 +81,41 @@ Functions Sort by function coverage Branches Sort by branch coverage + + RuleConditionalTransferLightMultiTokenBase.sol + +
98.9%98.9%
+ + 98.9 % + 91 / 92 + 96.4 % + 27 / 28 + 81.0 % + 17 / 21 + RuleConditionalTransferLightApprovalBase.sol -
94.6%94.6%
+
95.3%95.3%
+ + 95.3 % + 41 / 43 + 83.3 % + 10 / 12 + 100.0 % + 7 / 7 + + + RuleMintAllowanceBase.sol + +
98.2%98.2%
- 94.6 % - 35 / 37 - 81.8 % - 9 / 11 + 98.2 % + 56 / 57 + 95.2 % + 20 / 21 100.0 % - 5 / 5 + 9 / 9 RuleConditionalTransferLightBase.sol @@ -99,11 +123,11 @@
100.0%
100.0 % - 40 / 40 + 52 / 52 100.0 % - 13 / 13 + 16 / 16 100.0 % - 11 / 11 + 17 / 17 diff --git a/doc/coverage/coverage/src/rules/operation/abstract/index-sort-f.html b/doc/coverage/coverage/src/rules/operation/abstract/index-sort-f.html index 4c7f0d8..67d2fea 100644 --- a/doc/coverage/coverage/src/rules/operation/abstract/index-sort-f.html +++ b/doc/coverage/coverage/src/rules/operation/abstract/index-sort-f.html @@ -31,27 +31,27 @@ lcov.info Lines: - 75 - 77 - 97.4 % + 240 + 244 + 98.4 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 22 - 24 - 91.7 % + 73 + 77 + 94.8 % Branches: - 16 - 16 - 100.0 % + 50 + 54 + 92.6 % @@ -84,14 +84,38 @@ RuleConditionalTransferLightApprovalBase.sol -
94.6%94.6%
+
95.3%95.3%
- 94.6 % - 35 / 37 - 81.8 % - 9 / 11 + 95.3 % + 41 / 43 + 83.3 % + 10 / 12 100.0 % - 5 / 5 + 7 / 7 + + + RuleMintAllowanceBase.sol + +
98.2%98.2%
+ + 98.2 % + 56 / 57 + 95.2 % + 20 / 21 + 100.0 % + 9 / 9 + + + RuleConditionalTransferLightMultiTokenBase.sol + +
98.9%98.9%
+ + 98.9 % + 91 / 92 + 96.4 % + 27 / 28 + 81.0 % + 17 / 21 RuleConditionalTransferLightBase.sol @@ -99,11 +123,11 @@
100.0%
100.0 % - 40 / 40 + 52 / 52 100.0 % - 13 / 13 + 16 / 16 100.0 % - 11 / 11 + 17 / 17 diff --git a/doc/coverage/coverage/src/rules/operation/abstract/index-sort-l.html b/doc/coverage/coverage/src/rules/operation/abstract/index-sort-l.html index 68c0a99..4f7d8df 100644 --- a/doc/coverage/coverage/src/rules/operation/abstract/index-sort-l.html +++ b/doc/coverage/coverage/src/rules/operation/abstract/index-sort-l.html @@ -31,27 +31,27 @@ lcov.info Lines: - 75 - 77 - 97.4 % + 240 + 244 + 98.4 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 22 - 24 - 91.7 % + 73 + 77 + 94.8 % Branches: - 16 - 16 - 100.0 % + 50 + 54 + 92.6 % @@ -84,14 +84,38 @@ RuleConditionalTransferLightApprovalBase.sol -
94.6%94.6%
+
95.3%95.3%
- 94.6 % - 35 / 37 - 81.8 % - 9 / 11 + 95.3 % + 41 / 43 + 83.3 % + 10 / 12 100.0 % - 5 / 5 + 7 / 7 + + + RuleMintAllowanceBase.sol + +
98.2%98.2%
+ + 98.2 % + 56 / 57 + 95.2 % + 20 / 21 + 100.0 % + 9 / 9 + + + RuleConditionalTransferLightMultiTokenBase.sol + +
98.9%98.9%
+ + 98.9 % + 91 / 92 + 96.4 % + 27 / 28 + 81.0 % + 17 / 21 RuleConditionalTransferLightBase.sol @@ -99,11 +123,11 @@
100.0%
100.0 % - 40 / 40 + 52 / 52 100.0 % - 13 / 13 + 16 / 16 100.0 % - 11 / 11 + 17 / 17 diff --git a/doc/coverage/coverage/src/rules/operation/abstract/index.html b/doc/coverage/coverage/src/rules/operation/abstract/index.html index 3cdfea7..92e56ce 100644 --- a/doc/coverage/coverage/src/rules/operation/abstract/index.html +++ b/doc/coverage/coverage/src/rules/operation/abstract/index.html @@ -31,27 +31,27 @@ lcov.info Lines: - 75 - 77 - 97.4 % + 240 + 244 + 98.4 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 22 - 24 - 91.7 % + 73 + 77 + 94.8 % Branches: - 16 - 16 - 100.0 % + 50 + 54 + 92.6 % @@ -84,14 +84,14 @@ RuleConditionalTransferLightApprovalBase.sol -
94.6%94.6%
+
95.3%95.3%
- 94.6 % - 35 / 37 - 81.8 % - 9 / 11 + 95.3 % + 41 / 43 + 83.3 % + 10 / 12 100.0 % - 5 / 5 + 7 / 7 RuleConditionalTransferLightBase.sol @@ -99,11 +99,35 @@
100.0%
100.0 % - 40 / 40 + 52 / 52 + 100.0 % + 16 / 16 100.0 % - 13 / 13 + 17 / 17 + + + RuleConditionalTransferLightMultiTokenBase.sol + +
98.9%98.9%
+ + 98.9 % + 91 / 92 + 96.4 % + 27 / 28 + 81.0 % + 17 / 21 + + + RuleMintAllowanceBase.sol + +
98.2%98.2%
+ + 98.2 % + 56 / 57 + 95.2 % + 20 / 21 100.0 % - 11 / 11 + 9 / 9 diff --git a/doc/coverage/coverage/src/rules/operation/index-sort-b.html b/doc/coverage/coverage/src/rules/operation/index-sort-b.html index a9a2930..8359c44 100644 --- a/doc/coverage/coverage/src/rules/operation/index-sort-b.html +++ b/doc/coverage/coverage/src/rules/operation/index-sort-b.html @@ -31,18 +31,18 @@ lcov.info Lines: - 18 - 18 - 100.0 % + 47 + 50 + 94.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 6 - 6 - 100.0 % + 19 + 22 + 86.4 % @@ -82,29 +82,77 @@ Branches Sort by branch coverage - RuleConditionalTransferLight.sol + RuleConditionalTransferLightMultiTokenOwnable2Step.sol + +
75.0%75.0%
+ + 75.0 % + 6 / 8 + 33.3 % + 1 / 3 + - + 0 / 0 + + + RuleMintAllowanceOwnable2Step.sol
100.0%
100.0 % - 9 / 9 + 8 / 8 100.0 % - 3 / 3 + 4 / 4 - 0 / 0 RuleConditionalTransferLightOwnable2Step.sol + +
88.9%88.9%
+ + 88.9 % + 8 / 9 + 75.0 % + 3 / 4 + - + 0 / 0 + + + RuleConditionalTransferLight.sol
100.0%
100.0 % 9 / 9 100.0 % + 4 / 4 + - + 0 / 0 + + + RuleConditionalTransferLightMultiToken.sol + +
100.0%
+ + 100.0 % + 8 / 8 + 100.0 % 3 / 3 - 0 / 0 + + RuleMintAllowance.sol + +
100.0%
+ + 100.0 % + 8 / 8 + 100.0 % + 4 / 4 + - + 0 / 0 +
diff --git a/doc/coverage/coverage/src/rules/operation/index-sort-f.html b/doc/coverage/coverage/src/rules/operation/index-sort-f.html index b5619fb..959a4ad 100644 --- a/doc/coverage/coverage/src/rules/operation/index-sort-f.html +++ b/doc/coverage/coverage/src/rules/operation/index-sort-f.html @@ -31,18 +31,18 @@ lcov.info Lines: - 18 - 18 - 100.0 % + 47 + 50 + 94.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 6 - 6 - 100.0 % + 19 + 22 + 86.4 % @@ -82,26 +82,74 @@ Branches Sort by branch coverage - RuleConditionalTransferLight.sol + RuleConditionalTransferLightMultiTokenOwnable2Step.sol + +
75.0%75.0%
+ + 75.0 % + 6 / 8 + 33.3 % + 1 / 3 + - + 0 / 0 + + + RuleConditionalTransferLightOwnable2Step.sol + +
88.9%88.9%
+ + 88.9 % + 8 / 9 + 75.0 % + 3 / 4 + - + 0 / 0 + + + RuleConditionalTransferLightMultiToken.sol
100.0%
100.0 % - 9 / 9 + 8 / 8 100.0 % 3 / 3 - 0 / 0 - RuleConditionalTransferLightOwnable2Step.sol + RuleMintAllowanceOwnable2Step.sol + +
100.0%
+ + 100.0 % + 8 / 8 + 100.0 % + 4 / 4 + - + 0 / 0 + + + RuleConditionalTransferLight.sol
100.0%
100.0 % 9 / 9 100.0 % - 3 / 3 + 4 / 4 + - + 0 / 0 + + + RuleMintAllowance.sol + +
100.0%
+ + 100.0 % + 8 / 8 + 100.0 % + 4 / 4 - 0 / 0 diff --git a/doc/coverage/coverage/src/rules/operation/index-sort-l.html b/doc/coverage/coverage/src/rules/operation/index-sort-l.html index 55cbbfe..3116ed0 100644 --- a/doc/coverage/coverage/src/rules/operation/index-sort-l.html +++ b/doc/coverage/coverage/src/rules/operation/index-sort-l.html @@ -31,18 +31,18 @@ lcov.info Lines: - 18 - 18 - 100.0 % + 47 + 50 + 94.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 6 - 6 - 100.0 % + 19 + 22 + 86.4 % @@ -82,26 +82,74 @@ Branches Sort by branch coverage - RuleConditionalTransferLight.sol + RuleConditionalTransferLightMultiTokenOwnable2Step.sol + +
75.0%75.0%
+ + 75.0 % + 6 / 8 + 33.3 % + 1 / 3 + - + 0 / 0 + + + RuleConditionalTransferLightOwnable2Step.sol + +
88.9%88.9%
+ + 88.9 % + 8 / 9 + 75.0 % + 3 / 4 + - + 0 / 0 + + + RuleMintAllowanceOwnable2Step.sol
100.0%
100.0 % - 9 / 9 + 8 / 8 + 100.0 % + 4 / 4 + - + 0 / 0 + + + RuleConditionalTransferLightMultiToken.sol + +
100.0%
+ + 100.0 % + 8 / 8 100.0 % 3 / 3 - 0 / 0 - RuleConditionalTransferLightOwnable2Step.sol + RuleMintAllowance.sol + +
100.0%
+ + 100.0 % + 8 / 8 + 100.0 % + 4 / 4 + - + 0 / 0 + + + RuleConditionalTransferLight.sol
100.0%
100.0 % 9 / 9 100.0 % - 3 / 3 + 4 / 4 - 0 / 0 diff --git a/doc/coverage/coverage/src/rules/operation/index.html b/doc/coverage/coverage/src/rules/operation/index.html index 43751bf..570fecd 100644 --- a/doc/coverage/coverage/src/rules/operation/index.html +++ b/doc/coverage/coverage/src/rules/operation/index.html @@ -31,18 +31,18 @@ lcov.info Lines: - 18 - 18 - 100.0 % + 47 + 50 + 94.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 6 - 6 - 100.0 % + 19 + 22 + 86.4 % @@ -89,19 +89,67 @@ 100.0 % 9 / 9 100.0 % + 4 / 4 + - + 0 / 0 + + + RuleConditionalTransferLightMultiToken.sol + +
100.0%
+ + 100.0 % + 8 / 8 + 100.0 % 3 / 3 - 0 / 0 + + RuleConditionalTransferLightMultiTokenOwnable2Step.sol + +
75.0%75.0%
+ + 75.0 % + 6 / 8 + 33.3 % + 1 / 3 + - + 0 / 0 + RuleConditionalTransferLightOwnable2Step.sol + +
88.9%88.9%
+ + 88.9 % + 8 / 9 + 75.0 % + 3 / 4 + - + 0 / 0 + + + RuleMintAllowance.sol
100.0%
100.0 % - 9 / 9 + 8 / 8 100.0 % - 3 / 3 + 4 / 4 + - + 0 / 0 + + + RuleMintAllowanceOwnable2Step.sol + +
100.0%
+ + 100.0 % + 8 / 8 + 100.0 % + 4 / 4 - 0 / 0 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol.func-sort-c.html index b023fd5..1d6c527 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol.func-sort-c.html @@ -31,13 +31,13 @@ lcov.info Lines: - 34 - 36 - 94.4 % + 35 + 37 + 94.6 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 13 @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 6 + 6 100.0 % @@ -69,19 +69,19 @@ Hit count Sort by hit count - RuleAddressSet._authorizeAddressListAdd + RuleAddressSet._authorizeAddressListAdd 0 - RuleAddressSet._authorizeAddressListRemove + RuleAddressSet._authorizeAddressListRemove 0 - RuleAddressSet.contains - 2 + RuleAddressSet.contains + 4 - RuleAddressSet._msgData + RuleAddressSet._msgData 6 @@ -89,44 +89,44 @@ 12 - RuleAddressSet.removeAddress + RuleAddressSet.removeAddress 12 - RuleAddressSet.isAddressListed - 73 + RuleAddressSet.isAddressListed + 79 - RuleAddressSet.areAddressesListed - 108 + RuleAddressSet.addAddress + 132 - RuleAddressSet.addAddress - 117 + RuleAddressSet.areAddressesListed + 155 - RuleAddressSet.removeAddresses + RuleAddressSet.removeAddresses 260 - RuleAddressSet.addAddresses - 275 + RuleAddressSet.addAddresses + 278 RuleAddressSet.onlyAddressListAdd - 275 + 278 - RuleAddressSet.listedAddressCount - 536 + RuleAddressSet.listedAddressCount + 543 - RuleAddressSet._msgSender - 931 + RuleAddressSet._msgSender + 994 - RuleAddressSet._contextSuffixLength - 939 + RuleAddressSet._contextSuffixLength + 1002
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol.func.html index 3d6ae02..da92974 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol.func.html @@ -31,13 +31,13 @@ lcov.info Lines: - 34 - 36 - 94.4 % + 35 + 37 + 94.6 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 13 @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 6 + 6 100.0 % @@ -69,63 +69,63 @@ Hit count Sort by hit count - RuleAddressSet._authorizeAddressListAdd + RuleAddressSet._authorizeAddressListAdd 0 - RuleAddressSet._authorizeAddressListRemove + RuleAddressSet._authorizeAddressListRemove 0 - RuleAddressSet._contextSuffixLength - 939 + RuleAddressSet._contextSuffixLength + 1002 - RuleAddressSet._msgData + RuleAddressSet._msgData 6 - RuleAddressSet._msgSender - 931 + RuleAddressSet._msgSender + 994 - RuleAddressSet.addAddress - 117 + RuleAddressSet.addAddress + 132 - RuleAddressSet.addAddresses - 275 + RuleAddressSet.addAddresses + 278 - RuleAddressSet.areAddressesListed - 108 + RuleAddressSet.areAddressesListed + 155 - RuleAddressSet.contains - 2 + RuleAddressSet.contains + 4 - RuleAddressSet.isAddressListed - 73 + RuleAddressSet.isAddressListed + 79 - RuleAddressSet.listedAddressCount - 536 + RuleAddressSet.listedAddressCount + 543 RuleAddressSet.onlyAddressListAdd - 275 + 278 RuleAddressSet.onlyAddressListRemove 12 - RuleAddressSet.removeAddress + RuleAddressSet.removeAddress 12 - RuleAddressSet.removeAddresses + RuleAddressSet.removeAddresses 260 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol.gcov.html index 496a79c..82998c9 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol.gcov.html @@ -31,13 +31,13 @@ lcov.info Lines: - 34 - 36 - 94.4 % + 35 + 37 + 94.6 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 13 @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 6 + 6 100.0 % @@ -90,8 +90,8 @@ 19 : : 20 : : abstract contract RuleAddressSet is 21 : : MetaTxModuleStandalone, - 22 : : RuleAddressSetInternal, - 23 : : RuleAddressSetInvariantStorage, + 22 : : RuleAddressSetInvariantStorage, + 23 : : RuleAddressSetInternal, 24 : : IAddressList 25 : : { 26 : : /*////////////////////////////////////////////////////////////// @@ -108,8 +108,8 @@ 37 : : ACCESS CONTROL 38 : : //////////////////////////////////////////////////////////////*/ 39 : : - 40 : 275 : modifier onlyAddressListAdd() { - 41 : 275 : _authorizeAddressListAdd(); + 40 : 278 : modifier onlyAddressListAdd() { + 41 : 278 : _authorizeAddressListAdd(); 42 : : _; 43 : : } 44 : : @@ -118,121 +118,134 @@ 47 : : _; 48 : : } 49 : : - 50 : 0 : function _authorizeAddressListAdd() internal view virtual; - 51 : : - 52 : 0 : function _authorizeAddressListRemove() internal view virtual; + 50 : : /*////////////////////////////////////////////////////////////// + 51 : : PUBLIC FUNCTIONS + 52 : : //////////////////////////////////////////////////////////////*/ 53 : : - 54 : : /*////////////////////////////////////////////////////////////// - 55 : : PUBLIC FUNCTIONS - 56 : : //////////////////////////////////////////////////////////////*/ - 57 : : - 58 : : /** - 59 : : * @notice Adds multiple addresses to the set. - 60 : : * @dev - 61 : : * - Does not revert if an address is already listed. - 62 : : * - Accessible only by accounts with the `ADDRESS_LIST_ADD_ROLE`. - 63 : : * @param targetAddresses Array of addresses to be added. - 64 : : */ - 65 : 275 : function addAddresses(address[] calldata targetAddresses) public onlyAddressListAdd { - 66 : 274 : _addAddresses(targetAddresses); - 67 : 274 : emit AddAddresses(targetAddresses); - 68 : : } - 69 : : - 70 : : /** - 71 : : * @notice Removes multiple addresses from the set. - 72 : : * @dev - 73 : : * - Does not revert if an address is not listed. - 74 : : * - Accessible only by accounts with the `ADDRESS_LIST_REMOVE_ROLE`. - 75 : : * @param targetAddresses Array of addresses to remove. - 76 : : */ - 77 : 260 : function removeAddresses(address[] calldata targetAddresses) public onlyAddressListRemove { - 78 : 259 : _removeAddresses(targetAddresses); - 79 : 259 : emit RemoveAddresses(targetAddresses); - 80 : : } - 81 : : - 82 : : /** - 83 : : * @notice Adds a single address to the set. - 84 : : * @dev - 85 : : * - Reverts if the address is already listed. - 86 : : * - Accessible only by accounts with the `ADDRESS_LIST_ADD_ROLE`. - 87 : : * @param targetAddress The address to be added. - 88 : : */ - 89 : 117 : function addAddress(address targetAddress) public onlyAddressListAdd { - 90 [ + + ]: 112 : require(!_isAddressListed(targetAddress), RuleAddressSet_AddressAlreadyListed()); - 91 : 111 : _addAddress(targetAddress); - 92 : 111 : emit AddAddress(targetAddress); - 93 : : } - 94 : : - 95 : : /** - 96 : : * @notice Removes a single address from the set. - 97 : : * @dev - 98 : : * - Reverts if the address is not listed. - 99 : : * - Accessible only by accounts with the `ADDRESS_LIST_REMOVE_ROLE`. - 100 : : * @param targetAddress The address to be removed. - 101 : : */ - 102 : 12 : function removeAddress(address targetAddress) public onlyAddressListRemove { - 103 [ + + ]: 7 : require(_isAddressListed(targetAddress), RuleAddressSet_AddressNotFound()); - 104 : 6 : _removeAddress(targetAddress); - 105 : 6 : emit RemoveAddress(targetAddress); - 106 : : } - 107 : : - 108 : : /** - 109 : : * @notice Returns the total number of currently listed addresses. - 110 : : * @return count The number of listed addresses. - 111 : : */ - 112 : 536 : function listedAddressCount() public view returns (uint256 count) { - 113 : 536 : count = _listedAddressCount(); - 114 : : } - 115 : : - 116 : : /** - 117 : : * @notice Checks whether a specific address is currently listed. - 118 : : * @param targetAddress The address to check. - 119 : : * @return isListed True if listed, false otherwise. - 120 : : */ - 121 : 2 : function contains(address targetAddress) public view override(IIdentityRegistryContains) returns (bool isListed) { - 122 : 2 : isListed = _isAddressListed(targetAddress); - 123 : : } - 124 : : - 125 : : /** - 126 : : * @notice Checks whether a specific address is currently listed. - 127 : : * @param targetAddress The address to check. - 128 : : * @return isListed True if listed, false otherwise. - 129 : : */ - 130 : 73 : function isAddressListed(address targetAddress) public view returns (bool isListed) { - 131 : 393 : isListed = _isAddressListed(targetAddress); - 132 : : } - 133 : : - 134 : : /** - 135 : : * @notice Checks multiple addresses in a single call. - 136 : : * @param targetAddresses Array of addresses to check. - 137 : : * @return results Array of booleans corresponding to listing status. - 138 : : */ - 139 : 108 : function areAddressesListed(address[] memory targetAddresses) public view returns (bool[] memory results) { - 140 : 108 : results = new bool[](targetAddresses.length); - 141 : 108 : for (uint256 i = 0; i < targetAddresses.length; ++i) { - 142 : 244 : results[i] = _isAddressListed(targetAddresses[i]); - 143 : : } - 144 : : } - 145 : : - 146 : : /*////////////////////////////////////////////////////////////// - 147 : : INTERNAL FUNCTIONS - 148 : : //////////////////////////////////////////////////////////////*/ - 149 : : - 150 : : /// @inheritdoc ERC2771Context - 151 : 931 : function _msgSender() internal view virtual override(ERC2771Context) returns (address sender) { - 152 : 931 : return ERC2771Context._msgSender(); - 153 : : } - 154 : : - 155 : : /// @inheritdoc ERC2771Context - 156 : 6 : function _msgData() internal view virtual override(ERC2771Context) returns (bytes calldata) { - 157 : 6 : return ERC2771Context._msgData(); - 158 : : } - 159 : : - 160 : : /// @inheritdoc ERC2771Context - 161 : 939 : function _contextSuffixLength() internal view virtual override(ERC2771Context) returns (uint256) { - 162 : 939 : return ERC2771Context._contextSuffixLength(); - 163 : : } - 164 : : } + 54 : : /** + 55 : : * @notice Adds multiple addresses to the set. + 56 : : * @dev + 57 : : * - Does not revert if an address is already listed. + 58 : : * - Accessible only by accounts with the `ADDRESS_LIST_ADD_ROLE`. + 59 : : * @param targetAddresses Array of addresses to be added. + 60 : : */ + 61 : 278 : function addAddresses(address[] calldata targetAddresses) public onlyAddressListAdd { + 62 : 277 : _addAddresses(targetAddresses); + 63 : 275 : emit AddAddresses(targetAddresses); + 64 : : } + 65 : : + 66 : : /** + 67 : : * @notice Removes multiple addresses from the set. + 68 : : * @dev + 69 : : * - Does not revert if an address is not listed. + 70 : : * - Accessible only by accounts with the `ADDRESS_LIST_REMOVE_ROLE`. + 71 : : * @param targetAddresses Array of addresses to remove. + 72 : : */ + 73 : 260 : function removeAddresses(address[] calldata targetAddresses) public onlyAddressListRemove { + 74 : 259 : _removeAddresses(targetAddresses); + 75 : 259 : emit RemoveAddresses(targetAddresses); + 76 : : } + 77 : : + 78 : : /** + 79 : : * @notice Adds a single address to the set. + 80 : : * @dev + 81 : : * - Reverts if the address is already listed. + 82 : : * - Accessible only by accounts with the `ADDRESS_LIST_ADD_ROLE`. + 83 : : * @param targetAddress The address to be added. + 84 : : */ + 85 : 132 : function addAddress(address targetAddress) public onlyAddressListAdd { + 86 [ + + ]: 127 : require(targetAddress != address(0), RuleAddressSet_ZeroAddressNotAllowed()); + 87 [ + + ]: 125 : require(!_isAddressListed(targetAddress), RuleAddressSet_AddressAlreadyListed()); + 88 : 124 : _addAddress(targetAddress); + 89 : 124 : emit AddAddress(targetAddress); + 90 : : } + 91 : : + 92 : : /** + 93 : : * @notice Removes a single address from the set. + 94 : : * @dev + 95 : : * - Reverts if the address is not listed. + 96 : : * - Accessible only by accounts with the `ADDRESS_LIST_REMOVE_ROLE`. + 97 : : * @param targetAddress The address to be removed. + 98 : : */ + 99 : 12 : function removeAddress(address targetAddress) public onlyAddressListRemove { + 100 [ + + ]: 7 : require(_isAddressListed(targetAddress), RuleAddressSet_AddressNotFound()); + 101 : 6 : _removeAddress(targetAddress); + 102 : 6 : emit RemoveAddress(targetAddress); + 103 : : } + 104 : : + 105 : : /** + 106 : : * @notice Returns the total number of currently listed addresses. + 107 : : * @return count The number of listed addresses. + 108 : : */ + 109 : 543 : function listedAddressCount() public view returns (uint256 count) { + 110 : 543 : count = _listedAddressCount(); + 111 : : } + 112 : : + 113 : : /** + 114 : : * @notice Checks whether a specific address is currently listed. + 115 : : * @param targetAddress The address to check. + 116 : : * @return isListed True if listed, false otherwise. + 117 : : */ + 118 : 4 : function contains(address targetAddress) public view override(IIdentityRegistryContains) returns (bool isListed) { + 119 : 4 : isListed = _isAddressListed(targetAddress); + 120 : : } + 121 : : + 122 : : /** + 123 : : * @notice Checks whether a specific address is currently listed. + 124 : : * @param targetAddress The address to check. + 125 : : * @return isListed True if listed, false otherwise. + 126 : : */ + 127 : 79 : function isAddressListed(address targetAddress) public view returns (bool isListed) { + 128 : 577 : isListed = _isAddressListed(targetAddress); + 129 : : } + 130 : : + 131 : : /** + 132 : : * @notice Checks multiple addresses in a single call. + 133 : : * @param targetAddresses Array of addresses to check. + 134 : : * @return results Array of booleans corresponding to listing status. + 135 : : */ + 136 : 155 : function areAddressesListed(address[] memory targetAddresses) public view returns (bool[] memory results) { + 137 : 155 : results = new bool[](targetAddresses.length); + 138 : 155 : for (uint256 i = 0; i < targetAddresses.length; ++i) { + 139 : 345 : results[i] = _isAddressListed(targetAddresses[i]); + 140 : : } + 141 : : } + 142 : : + 143 : : /*////////////////////////////////////////////////////////////// + 144 : : INTERNAL FUNCTIONS + 145 : : //////////////////////////////////////////////////////////////*/ + 146 : : + 147 : : /** + 148 : : * @notice Authorizes the caller to add addresses to the set; reverts if unauthorized. + 149 : : */ + 150 : 0 : function _authorizeAddressListAdd() internal view virtual; + 151 : : + 152 : : /** + 153 : : * @notice Authorizes the caller to remove addresses from the set; reverts if unauthorized. + 154 : : */ + 155 : 0 : function _authorizeAddressListRemove() internal view virtual; + 156 : : + 157 : : /** + 158 : : * @inheritdoc ERC2771Context + 159 : : */ + 160 : 994 : function _msgSender() internal view virtual override(ERC2771Context) returns (address sender) { + 161 : 994 : return ERC2771Context._msgSender(); + 162 : : } + 163 : : + 164 : : /** + 165 : : * @inheritdoc ERC2771Context + 166 : : */ + 167 : 6 : function _msgData() internal view virtual override(ERC2771Context) returns (bytes calldata) { + 168 : 6 : return ERC2771Context._msgData(); + 169 : : } + 170 : : + 171 : : /** + 172 : : * @inheritdoc ERC2771Context + 173 : : */ + 174 : 1002 : function _contextSuffixLength() internal view virtual override(ERC2771Context) returns (uint256) { + 175 : 1002 : return ERC2771Context._contextSuffixLength(); + 176 : : } + 177 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol.func-sort-c.html index 05dd139..575a4c8 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol.func-sort-c.html @@ -31,13 +31,13 @@ lcov.info Lines: - 18 - 18 + 19 + 19 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 6 @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 6 + 6 100.0 % @@ -69,28 +69,28 @@ Hit count Sort by hit count - RuleAddressSetInternal._removeAddress + RuleAddressSetInternal._removeAddress 6 - RuleAddressSetInternal._addAddress - 113 + RuleAddressSetInternal._addAddress + 124 - RuleAddressSetInternal._removeAddresses + RuleAddressSetInternal._removeAddresses 259 - RuleAddressSetInternal._addAddresses - 274 + RuleAddressSetInternal._addAddresses + 277 - RuleAddressSetInternal._listedAddressCount - 536 + RuleAddressSetInternal._listedAddressCount + 543 - RuleAddressSetInternal._isAddressListed - 773 + RuleAddressSetInternal._isAddressListed + 1093
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol.func.html index e95d122..71364b6 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol.func.html @@ -31,13 +31,13 @@ lcov.info Lines: - 18 - 18 + 19 + 19 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 6 @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 6 + 6 100.0 % @@ -69,27 +69,27 @@ Hit count Sort by hit count - RuleAddressSetInternal._addAddress - 113 + RuleAddressSetInternal._addAddress + 124 - RuleAddressSetInternal._addAddresses - 274 + RuleAddressSetInternal._addAddresses + 277 - RuleAddressSetInternal._isAddressListed - 773 + RuleAddressSetInternal._isAddressListed + 1093 - RuleAddressSetInternal._listedAddressCount - 536 + RuleAddressSetInternal._listedAddressCount + 543 - RuleAddressSetInternal._removeAddress + RuleAddressSetInternal._removeAddress 6 - RuleAddressSetInternal._removeAddresses + RuleAddressSetInternal._removeAddresses 259 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol.gcov.html index 520fea8..05cae7b 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol.gcov.html @@ -31,13 +31,13 @@ lcov.info Lines: - 18 - 18 + 19 + 19 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 6 @@ -49,8 +49,8 @@ Branches: - 4 - 4 + 6 + 6 100.0 % @@ -74,103 +74,113 @@ 3 : : 4 : : /* ==== OpenZeppelin === */ 5 : : import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - 6 : : - 7 : : /** - 8 : : * @title Rule Address Set (Internal) - 9 : : * @notice Internal utility for managing a set of rule-related addresses. - 10 : : * @dev - 11 : : * - Uses OpenZeppelin's EnumerableSet for efficient enumeration. - 12 : : * - Designed for internal inheritance and logic composition. - 13 : : * - Batch operations do not revert when individual entries are invalid. - 14 : : */ - 15 : : abstract contract RuleAddressSetInternal { - 16 : : using EnumerableSet for EnumerableSet.AddressSet; - 17 : : - 18 : : /*////////////////////////////////////////////////////////////// - 19 : : STATE VARIABLES - 20 : : //////////////////////////////////////////////////////////////*/ - 21 : : - 22 : : /// @dev Storage for all listed addresses. - 23 : : EnumerableSet.AddressSet private _listedAddresses; - 24 : : - 25 : : /*////////////////////////////////////////////////////////////// - 26 : : INTERNAL FUNCTIONS - 27 : : //////////////////////////////////////////////////////////////*/ - 28 : : - 29 : : /** - 30 : : * @notice Adds multiple addresses to the set. - 31 : : * @dev - 32 : : * - Does not revert if an address is already listed. - 33 : : * - Skips existing entries silently. - 34 : : * @param addressesToAdd The array of addresses to add. - 35 : : * @return added The number of newly added addresses. - 36 : : * @return skipped The number of addresses that were already listed. - 37 : : */ - 38 : 274 : function _addAddresses(address[] calldata addressesToAdd) internal returns (uint256 added, uint256 skipped) { - 39 : 274 : for (uint256 i = 0; i < addressesToAdd.length; ++i) { - 40 [ + + ]: 806 : if (_listedAddresses.add(addressesToAdd[i])) { - 41 : 548 : added += 1; - 42 : : } else { - 43 : 258 : skipped += 1; - 44 : : } - 45 : : } - 46 : : } - 47 : : - 48 : : /** - 49 : : * @notice Removes multiple addresses from the set. - 50 : : * @dev - 51 : : * - Does not revert if an address is not found. - 52 : : * - Skips non-existing entries silently. - 53 : : * @param addressesToRemove The array of addresses to remove. - 54 : : * @return removed The number of addresses removed. - 55 : : * @return skipped The number of addresses that were not listed. - 56 : : */ - 57 : 259 : function _removeAddresses(address[] calldata addressesToRemove) - 58 : : internal - 59 : : returns (uint256 removed, uint256 skipped) - 60 : : { - 61 : 259 : for (uint256 i = 0; i < addressesToRemove.length; ++i) { - 62 [ + + ]: 775 : if (_listedAddresses.remove(addressesToRemove[i])) { - 63 : 518 : removed += 1; - 64 : : } else { - 65 : 257 : skipped += 1; - 66 : : } - 67 : : } - 68 : : } - 69 : : - 70 : : /** - 71 : : * @notice Adds a single address to the set. - 72 : : * @param targetAddress The address to add. - 73 : : */ - 74 : 113 : function _addAddress(address targetAddress) internal virtual { - 75 : 113 : _listedAddresses.add(targetAddress); - 76 : : } - 77 : : - 78 : : /** - 79 : : * @notice Removes a single address from the set. - 80 : : * @param targetAddress The address to remove. - 81 : : */ - 82 : 6 : function _removeAddress(address targetAddress) internal virtual { - 83 : 6 : _listedAddresses.remove(targetAddress); - 84 : : } - 85 : : - 86 : : /** - 87 : : * @notice Returns the total number of listed addresses. - 88 : : * @return count The number of listed addresses. - 89 : : */ - 90 : 536 : function _listedAddressCount() internal view virtual returns (uint256 count) { - 91 : 536 : count = _listedAddresses.length(); - 92 : : } - 93 : : - 94 : : /** - 95 : : * @notice Checks if an address is listed. - 96 : : * @param targetAddress The address to check. - 97 : : * @return isListed True if the address is listed, false otherwise. - 98 : : */ - 99 : 773 : function _isAddressListed(address targetAddress) internal view virtual returns (bool isListed) { - 100 : 773 : isListed = _listedAddresses.contains(targetAddress); - 101 : : } - 102 : : } + 6 : : import {RuleAddressSetInvariantStorage} from "./invariantStorage/RuleAddressSetInvariantStorage.sol"; + 7 : : + 8 : : /** + 9 : : * @title Rule Address Set (Internal) + 10 : : * @notice Internal utility for managing a set of rule-related addresses. + 11 : : * @dev + 12 : : * - Uses OpenZeppelin's EnumerableSet for efficient enumeration. + 13 : : * - Designed for internal inheritance and logic composition. + 14 : : * - Batch operations do not revert when individual entries are invalid. + 15 : : */ + 16 : : abstract contract RuleAddressSetInternal is RuleAddressSetInvariantStorage { + 17 : : using EnumerableSet for EnumerableSet.AddressSet; + 18 : : + 19 : : /*////////////////////////////////////////////////////////////// + 20 : : STATE VARIABLES + 21 : : //////////////////////////////////////////////////////////////*/ + 22 : : + 23 : : /** + 24 : : * @dev Storage for all listed addresses. + 25 : : */ + 26 : : EnumerableSet.AddressSet private _listedAddresses; + 27 : : + 28 : : /*////////////////////////////////////////////////////////////// + 29 : : INTERNAL FUNCTIONS + 30 : : //////////////////////////////////////////////////////////////*/ + 31 : : + 32 : : /** + 33 : : * @notice Adds multiple addresses to the set. + 34 : : * @dev + 35 : : * - Does not revert if an address is already listed. + 36 : : * - Skips existing entries silently. + 37 : : * @param addressesToAdd The array of addresses to add. + 38 : : * @return added The number of newly added addresses. + 39 : : * @return skipped The number of addresses that were already listed. + 40 : : */ + 41 : 277 : function _addAddresses(address[] calldata addressesToAdd) internal returns (uint256 added, uint256 skipped) { + 42 : 277 : for (uint256 i = 0; i < addressesToAdd.length; ++i) { + 43 : : // The zero address is the mint/burn sentinel, never a participant. It is REJECTED + 44 : : // rather than skipped: the batch convention skips *duplicates* (an idempotent no-op that + 45 : : // the emitted event still describes truthfully), but silently dropping address(0) would + 46 : : // make `AddAddresses` report a member that is not in the set — re-polluting the very + 47 : : // off-chain view this guard exists to keep clean. Mint/burn is governed by + 48 : : // allowMint/allowBurn, never by list membership. + 49 [ + + ]: 813 : require(addressesToAdd[i] != address(0), RuleAddressSet_ZeroAddressNotAllowed()); + 50 [ + + ]: 811 : if (_listedAddresses.add(addressesToAdd[i])) { + 51 : 551 : added += 1; + 52 : : } else { + 53 : 260 : skipped += 1; + 54 : : } + 55 : : } + 56 : : } + 57 : : + 58 : : /** + 59 : : * @notice Removes multiple addresses from the set. + 60 : : * @dev + 61 : : * - Does not revert if an address is not found. + 62 : : * - Skips non-existing entries silently. + 63 : : * @param addressesToRemove The array of addresses to remove. + 64 : : * @return removed The number of addresses removed. + 65 : : * @return skipped The number of addresses that were not listed. + 66 : : */ + 67 : 259 : function _removeAddresses(address[] calldata addressesToRemove) + 68 : : internal + 69 : : returns (uint256 removed, uint256 skipped) + 70 : : { + 71 : 259 : for (uint256 i = 0; i < addressesToRemove.length; ++i) { + 72 [ + + ]: 775 : if (_listedAddresses.remove(addressesToRemove[i])) { + 73 : 518 : removed += 1; + 74 : : } else { + 75 : 257 : skipped += 1; + 76 : : } + 77 : : } + 78 : : } + 79 : : + 80 : : /** + 81 : : * @notice Adds a single address to the set. + 82 : : * @param targetAddress The address to add. + 83 : : */ + 84 : 124 : function _addAddress(address targetAddress) internal virtual { + 85 : 124 : _listedAddresses.add(targetAddress); + 86 : : } + 87 : : + 88 : : /** + 89 : : * @notice Removes a single address from the set. + 90 : : * @param targetAddress The address to remove. + 91 : : */ + 92 : 6 : function _removeAddress(address targetAddress) internal virtual { + 93 : 6 : _listedAddresses.remove(targetAddress); + 94 : : } + 95 : : + 96 : : /** + 97 : : * @notice Returns the total number of listed addresses. + 98 : : * @return count The number of listed addresses. + 99 : : */ + 100 : 543 : function _listedAddressCount() internal view virtual returns (uint256 count) { + 101 : 543 : count = _listedAddresses.length(); + 102 : : } + 103 : : + 104 : : /** + 105 : : * @notice Checks if an address is listed. + 106 : : * @param targetAddress The address to check. + 107 : : * @return isListed True if the address is listed, false otherwise. + 108 : : */ + 109 : 1093 : function _isAddressListed(address targetAddress) internal view virtual returns (bool isListed) { + 110 : 1093 : isListed = _listedAddresses.contains(targetAddress); + 111 : : } + 112 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index-sort-b.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index-sort-b.html index b7b5003..d6eb283 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index-sort-b.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index-sort-b.html @@ -31,13 +31,13 @@ lcov.info Lines: - 52 54 - 96.3 % + 56 + 96.4 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 19 @@ -49,8 +49,8 @@ Branches: - 8 - 8 + 12 + 12 100.0 % @@ -82,28 +82,28 @@ Branches Sort by branch coverage - RuleAddressSet.sol + RuleAddressSetInternal.sol -
94.4%94.4%
+
100.0%
- 94.4 % - 34 / 36 - 86.7 % - 13 / 15 100.0 % - 4 / 4 + 19 / 19 + 100.0 % + 6 / 6 + 100.0 % + 6 / 6 - RuleAddressSetInternal.sol + RuleAddressSet.sol -
100.0%
+
94.6%94.6%
- 100.0 % - 18 / 18 + 94.6 % + 35 / 37 + 86.7 % + 13 / 15 100.0 % 6 / 6 - 100.0 % - 4 / 4 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index-sort-f.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index-sort-f.html index 0ec1d22..ab1542b 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index-sort-f.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index-sort-f.html @@ -31,13 +31,13 @@ lcov.info Lines: - 52 54 - 96.3 % + 56 + 96.4 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 19 @@ -49,8 +49,8 @@ Branches: - 8 - 8 + 12 + 12 100.0 % @@ -84,14 +84,14 @@ RuleAddressSet.sol -
94.4%94.4%
+
94.6%94.6%
- 94.4 % - 34 / 36 + 94.6 % + 35 / 37 86.7 % 13 / 15 100.0 % - 4 / 4 + 6 / 6 RuleAddressSetInternal.sol @@ -99,11 +99,11 @@
100.0%
100.0 % - 18 / 18 + 19 / 19 100.0 % 6 / 6 100.0 % - 4 / 4 + 6 / 6 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index-sort-l.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index-sort-l.html index 254ba12..383dd18 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index-sort-l.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index-sort-l.html @@ -31,13 +31,13 @@ lcov.info Lines: - 52 54 - 96.3 % + 56 + 96.4 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 19 @@ -49,8 +49,8 @@ Branches: - 8 - 8 + 12 + 12 100.0 % @@ -84,14 +84,14 @@ RuleAddressSet.sol -
94.4%94.4%
+
94.6%94.6%
- 94.4 % - 34 / 36 + 94.6 % + 35 / 37 86.7 % 13 / 15 100.0 % - 4 / 4 + 6 / 6 RuleAddressSetInternal.sol @@ -99,11 +99,11 @@
100.0%
100.0 % - 18 / 18 + 19 / 19 100.0 % 6 / 6 100.0 % - 4 / 4 + 6 / 6 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index.html index 2abdb78..85e2076 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleAddressSet/index.html @@ -31,13 +31,13 @@ lcov.info Lines: - 52 54 - 96.3 % + 56 + 96.4 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 19 @@ -49,8 +49,8 @@ Branches: - 8 - 8 + 12 + 12 100.0 % @@ -84,14 +84,14 @@ RuleAddressSet.sol -
94.4%94.4%
+
94.6%94.6%
- 94.4 % - 34 / 36 + 94.6 % + 35 / 37 86.7 % 13 / 15 100.0 % - 4 / 4 + 6 / 6 RuleAddressSetInternal.sol @@ -99,11 +99,11 @@
100.0%
100.0 % - 18 / 18 + 19 / 19 100.0 % 6 / 6 100.0 % - 4 / 4 + 6 / 6 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol.func-sort-c.html index 47d2037..56fb628 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol.func-sort-c.html @@ -31,13 +31,13 @@ lcov.info Lines: - 36 - 36 + 38 + 38 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 12 @@ -49,9 +49,9 @@ Branches: - 8 - 8 - 100.0 % + 10 + 12 + 83.3 % @@ -69,52 +69,52 @@ Hit count Sort by hit count - RuleERC2980Internal._removeFrozenlistAddresses + RuleERC2980Internal._removeFrozenlistAddresses 2 - RuleERC2980Internal._removeWhitelistAddresses + RuleERC2980Internal._removeWhitelistAddresses 3 - RuleERC2980Internal._addFrozenlistAddresses + RuleERC2980Internal._addFrozenlistAddresses 4 - RuleERC2980Internal._addWhitelistAddresses + RuleERC2980Internal._addWhitelistAddresses 4 - RuleERC2980Internal._frozenlistCount + RuleERC2980Internal._frozenlistCount 4 - RuleERC2980Internal._removeFrozenlistAddress + RuleERC2980Internal._removeFrozenlistAddress 4 - RuleERC2980Internal._removeWhitelistAddress + RuleERC2980Internal._removeWhitelistAddress 4 - RuleERC2980Internal._whitelistCount + RuleERC2980Internal._whitelistCount 5 - RuleERC2980Internal._addFrozenlistAddress - 18 + RuleERC2980Internal._addFrozenlistAddress + 19 - RuleERC2980Internal._addWhitelistAddress - 43 + RuleERC2980Internal._addWhitelistAddress + 44 - RuleERC2980Internal._isWhitelisted - 89 + RuleERC2980Internal._isWhitelisted + 115 - RuleERC2980Internal._isFrozen - 94 + RuleERC2980Internal._isFrozen + 163
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol.func.html index 64acba5..a4a1774 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol.func.html @@ -31,13 +31,13 @@ lcov.info Lines: - 36 - 36 + 38 + 38 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 12 @@ -49,9 +49,9 @@ Branches: - 8 - 8 - 100.0 % + 10 + 12 + 83.3 % @@ -69,51 +69,51 @@ Hit count Sort by hit count - RuleERC2980Internal._addFrozenlistAddress - 18 + RuleERC2980Internal._addFrozenlistAddress + 19 - RuleERC2980Internal._addFrozenlistAddresses + RuleERC2980Internal._addFrozenlistAddresses 4 - RuleERC2980Internal._addWhitelistAddress - 43 + RuleERC2980Internal._addWhitelistAddress + 44 - RuleERC2980Internal._addWhitelistAddresses + RuleERC2980Internal._addWhitelistAddresses 4 - RuleERC2980Internal._frozenlistCount + RuleERC2980Internal._frozenlistCount 4 - RuleERC2980Internal._isFrozen - 94 + RuleERC2980Internal._isFrozen + 163 - RuleERC2980Internal._isWhitelisted - 89 + RuleERC2980Internal._isWhitelisted + 115 - RuleERC2980Internal._removeFrozenlistAddress + RuleERC2980Internal._removeFrozenlistAddress 4 - RuleERC2980Internal._removeFrozenlistAddresses + RuleERC2980Internal._removeFrozenlistAddresses 2 - RuleERC2980Internal._removeWhitelistAddress + RuleERC2980Internal._removeWhitelistAddress 4 - RuleERC2980Internal._removeWhitelistAddresses + RuleERC2980Internal._removeWhitelistAddresses 3 - RuleERC2980Internal._whitelistCount + RuleERC2980Internal._whitelistCount 5 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol.gcov.html index fae617a..8996e6f 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol.gcov.html @@ -31,13 +31,13 @@ lcov.info Lines: - 36 - 36 + 38 + 38 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 12 @@ -49,9 +49,9 @@ Branches: - 8 - 8 - 100.0 % + 10 + 12 + 83.3 % @@ -74,121 +74,194 @@ 3 : : 4 : : /* ==== OpenZeppelin === */ 5 : : import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - 6 : : - 7 : : /** - 8 : : * @title RuleERC2980Internal - 9 : : * @notice Internal storage and helpers for two independent address sets: - 10 : : * a whitelist and a frozenlist, following the same pattern as {RuleAddressSetInternal}. - 11 : : * @dev - 12 : : * - Whitelist: only whitelisted addresses may receive tokens. - 13 : : * - Frozenlist: frozen addresses may neither send nor receive tokens. - 14 : : * - Batch operations do not revert when individual entries are already present or absent. - 15 : : */ - 16 : : abstract contract RuleERC2980Internal { - 17 : : using EnumerableSet for EnumerableSet.AddressSet; - 18 : : - 19 : : /*////////////////////////////////////////////////////////////// - 20 : : STATE VARIABLES - 21 : : //////////////////////////////////////////////////////////////*/ - 22 : : - 23 : : /// @dev Addresses allowed to receive tokens. - 24 : : EnumerableSet.AddressSet private _whitelist; - 25 : : - 26 : : /// @dev Addresses completely blocked from sending and receiving tokens. - 27 : : EnumerableSet.AddressSet private _frozenlist; + 6 : : import {RuleERC2980InvariantStorage} from "./invariantStorage/RuleERC2980InvariantStorage.sol"; + 7 : : + 8 : : /** + 9 : : * @title RuleERC2980Internal + 10 : : * @notice Internal storage and helpers for two independent address sets: + 11 : : * a whitelist and a frozenlist, following the same pattern as {RuleAddressSetInternal}. + 12 : : * @dev + 13 : : * - Whitelist: only whitelisted addresses may receive tokens. + 14 : : * - Frozenlist: frozen addresses may neither send nor receive tokens. + 15 : : * - Batch operations do not revert when individual entries are already present or absent. + 16 : : */ + 17 : : abstract contract RuleERC2980Internal is RuleERC2980InvariantStorage { + 18 : : using EnumerableSet for EnumerableSet.AddressSet; + 19 : : + 20 : : /*////////////////////////////////////////////////////////////// + 21 : : STATE VARIABLES + 22 : : //////////////////////////////////////////////////////////////*/ + 23 : : + 24 : : /** + 25 : : * @dev Addresses allowed to receive tokens. + 26 : : */ + 27 : : EnumerableSet.AddressSet private _whitelist; 28 : : - 29 : : /*////////////////////////////////////////////////////////////// - 30 : : WHITELIST — INTERNAL - 31 : : //////////////////////////////////////////////////////////////*/ - 32 : : - 33 : 4 : function _addWhitelistAddresses(address[] calldata addressesToAdd) - 34 : : internal - 35 : : returns (uint256 added, uint256 skipped) - 36 : : { - 37 : 4 : for (uint256 i = 0; i < addressesToAdd.length; ++i) { - 38 [ + + ]: 6 : if (_whitelist.add(addressesToAdd[i])) { - 39 : 5 : added += 1; - 40 : : } else { - 41 : 1 : skipped += 1; - 42 : : } - 43 : : } - 44 : : } - 45 : : - 46 : 3 : function _removeWhitelistAddresses(address[] calldata addressesToRemove) - 47 : : internal - 48 : : returns (uint256 removed, uint256 skipped) - 49 : : { - 50 : 3 : for (uint256 i = 0; i < addressesToRemove.length; ++i) { - 51 [ + + ]: 3 : if (_whitelist.remove(addressesToRemove[i])) { - 52 : 2 : removed += 1; - 53 : : } else { - 54 : 1 : skipped += 1; - 55 : : } - 56 : : } - 57 : : } - 58 : : - 59 : 43 : function _addWhitelistAddress(address targetAddress) internal virtual { - 60 : 43 : _whitelist.add(targetAddress); - 61 : : } - 62 : : - 63 : 4 : function _removeWhitelistAddress(address targetAddress) internal virtual { - 64 : 4 : _whitelist.remove(targetAddress); - 65 : : } - 66 : : - 67 : 89 : function _isWhitelisted(address targetAddress) internal view virtual returns (bool) { - 68 : 89 : return _whitelist.contains(targetAddress); - 69 : : } - 70 : : - 71 : 5 : function _whitelistCount() internal view virtual returns (uint256) { - 72 : 5 : return _whitelist.length(); - 73 : : } - 74 : : - 75 : : /*////////////////////////////////////////////////////////////// - 76 : : FROZENLIST — INTERNAL - 77 : : //////////////////////////////////////////////////////////////*/ + 29 : : /** + 30 : : * @dev Addresses completely blocked from sending and receiving tokens. + 31 : : */ + 32 : : EnumerableSet.AddressSet private _frozenlist; + 33 : : + 34 : : /*////////////////////////////////////////////////////////////// + 35 : : WHITELIST — INTERNAL + 36 : : //////////////////////////////////////////////////////////////*/ + 37 : : + 38 : : /** + 39 : : * @notice Adds multiple addresses to the whitelist, skipping any already present. + 40 : : * @param addressesToAdd Addresses to add to the whitelist. + 41 : : * @return added Number of addresses newly added. + 42 : : * @return skipped Number of addresses that were already whitelisted. + 43 : : */ + 44 : 4 : function _addWhitelistAddresses(address[] calldata addressesToAdd) + 45 : : internal + 46 : : returns (uint256 added, uint256 skipped) + 47 : : { + 48 : 4 : for (uint256 i = 0; i < addressesToAdd.length; ++i) { + 49 : : // The zero address is the mint/burn sentinel, never a participant. REJECTED rather than + 50 : : // skipped, so the emitted batch event can never report it as a list member. + 51 [ # + ]: 6 : require(addressesToAdd[i] != address(0), RuleERC2980_ZeroAddressNotAllowed()); + 52 [ + + ]: 6 : if (_whitelist.add(addressesToAdd[i])) { + 53 : 5 : added += 1; + 54 : : } else { + 55 : 1 : skipped += 1; + 56 : : } + 57 : : } + 58 : : } + 59 : : + 60 : : /** + 61 : : * @notice Removes multiple addresses from the whitelist, skipping any that are absent. + 62 : : * @param addressesToRemove Addresses to remove from the whitelist. + 63 : : * @return removed Number of addresses actually removed. + 64 : : * @return skipped Number of addresses that were not whitelisted. + 65 : : */ + 66 : 3 : function _removeWhitelistAddresses(address[] calldata addressesToRemove) + 67 : : internal + 68 : : returns (uint256 removed, uint256 skipped) + 69 : : { + 70 : 3 : for (uint256 i = 0; i < addressesToRemove.length; ++i) { + 71 [ + + ]: 3 : if (_whitelist.remove(addressesToRemove[i])) { + 72 : 2 : removed += 1; + 73 : : } else { + 74 : 1 : skipped += 1; + 75 : : } + 76 : : } + 77 : : } 78 : : - 79 : 4 : function _addFrozenlistAddresses(address[] calldata addressesToAdd) - 80 : : internal - 81 : : returns (uint256 added, uint256 skipped) - 82 : : { - 83 : 4 : for (uint256 i = 0; i < addressesToAdd.length; ++i) { - 84 [ + + ]: 6 : if (_frozenlist.add(addressesToAdd[i])) { - 85 : 5 : added += 1; - 86 : : } else { - 87 : 1 : skipped += 1; - 88 : : } - 89 : : } - 90 : : } - 91 : : - 92 : 2 : function _removeFrozenlistAddresses(address[] calldata addressesToRemove) - 93 : : internal - 94 : : returns (uint256 removed, uint256 skipped) - 95 : : { - 96 : 2 : for (uint256 i = 0; i < addressesToRemove.length; ++i) { - 97 [ + + ]: 2 : if (_frozenlist.remove(addressesToRemove[i])) { - 98 : 1 : removed += 1; - 99 : : } else { - 100 : 1 : skipped += 1; - 101 : : } - 102 : : } - 103 : : } - 104 : : - 105 : 18 : function _addFrozenlistAddress(address targetAddress) internal virtual { - 106 : 18 : _frozenlist.add(targetAddress); - 107 : : } - 108 : : - 109 : 4 : function _removeFrozenlistAddress(address targetAddress) internal virtual { - 110 : 4 : _frozenlist.remove(targetAddress); - 111 : : } - 112 : : - 113 : 94 : function _isFrozen(address targetAddress) internal view virtual returns (bool) { - 114 : 94 : return _frozenlist.contains(targetAddress); - 115 : : } - 116 : : - 117 : 4 : function _frozenlistCount() internal view virtual returns (uint256) { - 118 : 4 : return _frozenlist.length(); + 79 : : /** + 80 : : * @notice Adds a single address to the whitelist. + 81 : : * @param targetAddress Address to add to the whitelist. + 82 : : */ + 83 : 44 : function _addWhitelistAddress(address targetAddress) internal virtual { + 84 : 44 : _whitelist.add(targetAddress); + 85 : : } + 86 : : + 87 : : /** + 88 : : * @notice Removes a single address from the whitelist. + 89 : : * @param targetAddress Address to remove from the whitelist. + 90 : : */ + 91 : 4 : function _removeWhitelistAddress(address targetAddress) internal virtual { + 92 : 4 : _whitelist.remove(targetAddress); + 93 : : } + 94 : : + 95 : : /*////////////////////////////////////////////////////////////// + 96 : : FROZENLIST — INTERNAL + 97 : : //////////////////////////////////////////////////////////////*/ + 98 : : + 99 : : /** + 100 : : * @notice Adds multiple addresses to the frozenlist, skipping any already present. + 101 : : * @param addressesToAdd Addresses to add to the frozenlist. + 102 : : * @return added Number of addresses newly added. + 103 : : * @return skipped Number of addresses that were already frozen. + 104 : : */ + 105 : 4 : function _addFrozenlistAddresses(address[] calldata addressesToAdd) + 106 : : internal + 107 : : returns (uint256 added, uint256 skipped) + 108 : : { + 109 : 4 : for (uint256 i = 0; i < addressesToAdd.length; ++i) { + 110 : : // The zero address is the mint/burn sentinel, never a participant. REJECTED rather than + 111 : : // skipped, so the emitted batch event can never report it as a list member. + 112 [ # + ]: 6 : require(addressesToAdd[i] != address(0), RuleERC2980_ZeroAddressNotAllowed()); + 113 [ + + ]: 6 : if (_frozenlist.add(addressesToAdd[i])) { + 114 : 5 : added += 1; + 115 : : } else { + 116 : 1 : skipped += 1; + 117 : : } + 118 : : } 119 : : } - 120 : : } + 120 : : + 121 : : /** + 122 : : * @notice Removes multiple addresses from the frozenlist, skipping any that are absent. + 123 : : * @param addressesToRemove Addresses to remove from the frozenlist. + 124 : : * @return removed Number of addresses actually removed. + 125 : : * @return skipped Number of addresses that were not frozen. + 126 : : */ + 127 : 2 : function _removeFrozenlistAddresses(address[] calldata addressesToRemove) + 128 : : internal + 129 : : returns (uint256 removed, uint256 skipped) + 130 : : { + 131 : 2 : for (uint256 i = 0; i < addressesToRemove.length; ++i) { + 132 [ + + ]: 2 : if (_frozenlist.remove(addressesToRemove[i])) { + 133 : 1 : removed += 1; + 134 : : } else { + 135 : 1 : skipped += 1; + 136 : : } + 137 : : } + 138 : : } + 139 : : + 140 : : /** + 141 : : * @notice Adds a single address to the frozenlist. + 142 : : * @param targetAddress Address to add to the frozenlist. + 143 : : */ + 144 : 19 : function _addFrozenlistAddress(address targetAddress) internal virtual { + 145 : 19 : _frozenlist.add(targetAddress); + 146 : : } + 147 : : + 148 : : /** + 149 : : * @notice Removes a single address from the frozenlist. + 150 : : * @param targetAddress Address to remove from the frozenlist. + 151 : : */ + 152 : 4 : function _removeFrozenlistAddress(address targetAddress) internal virtual { + 153 : 4 : _frozenlist.remove(targetAddress); + 154 : : } + 155 : : + 156 : : /*////////////////////////////////////////////////////////////// + 157 : : VIEW — INTERNAL + 158 : : //////////////////////////////////////////////////////////////*/ + 159 : : + 160 : : /** + 161 : : * @notice Returns whether an address is whitelisted. + 162 : : * @param targetAddress Address to check. + 163 : : * @return True if the address is whitelisted. + 164 : : */ + 165 : 115 : function _isWhitelisted(address targetAddress) internal view virtual returns (bool) { + 166 : 115 : return _whitelist.contains(targetAddress); + 167 : : } + 168 : : + 169 : : /** + 170 : : * @notice Returns the number of whitelisted addresses. + 171 : : * @return The count of whitelisted addresses. + 172 : : */ + 173 : 5 : function _whitelistCount() internal view virtual returns (uint256) { + 174 : 5 : return _whitelist.length(); + 175 : : } + 176 : : + 177 : : /** + 178 : : * @notice Returns whether an address is frozen. + 179 : : * @param targetAddress Address to check. + 180 : : * @return True if the address is frozen. + 181 : : */ + 182 : 163 : function _isFrozen(address targetAddress) internal view virtual returns (bool) { + 183 : 163 : return _frozenlist.contains(targetAddress); + 184 : : } + 185 : : + 186 : : /** + 187 : : * @notice Returns the number of frozen addresses. + 188 : : * @return The count of frozen addresses. + 189 : : */ + 190 : 4 : function _frozenlistCount() internal view virtual returns (uint256) { + 191 : 4 : return _frozenlist.length(); + 192 : : } + 193 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index-sort-b.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index-sort-b.html index 443cd34..fb816b0 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index-sort-b.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index-sort-b.html @@ -31,13 +31,13 @@ lcov.info Lines: - 36 - 36 + 38 + 38 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 12 @@ -49,9 +49,9 @@ Branches: - 8 - 8 - 100.0 % + 10 + 12 + 83.3 % @@ -87,11 +87,11 @@
100.0%
100.0 % - 36 / 36 + 38 / 38 100.0 % 12 / 12 - 100.0 % - 8 / 8 + 83.3 % + 10 / 12 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index-sort-f.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index-sort-f.html index 8da3e22..a6d4ca1 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index-sort-f.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index-sort-f.html @@ -31,13 +31,13 @@ lcov.info Lines: - 36 - 36 + 38 + 38 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 12 @@ -49,9 +49,9 @@ Branches: - 8 - 8 - 100.0 % + 10 + 12 + 83.3 % @@ -87,11 +87,11 @@
100.0%
100.0 % - 36 / 36 + 38 / 38 100.0 % 12 / 12 - 100.0 % - 8 / 8 + 83.3 % + 10 / 12 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index-sort-l.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index-sort-l.html index fcb03d9..5efbf7a 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index-sort-l.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index-sort-l.html @@ -31,13 +31,13 @@ lcov.info Lines: - 36 - 36 + 38 + 38 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 12 @@ -49,9 +49,9 @@ Branches: - 8 - 8 - 100.0 % + 10 + 12 + 83.3 % @@ -87,11 +87,11 @@
100.0%
100.0 % - 36 / 36 + 38 / 38 100.0 % 12 / 12 - 100.0 % - 8 / 8 + 83.3 % + 10 / 12 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index.html b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index.html index 26b1739..7583b40 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/RuleERC2980/index.html @@ -31,13 +31,13 @@ lcov.info Lines: - 36 - 36 + 38 + 38 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 12 @@ -49,9 +49,9 @@ Branches: - 8 - 8 - 100.0 % + 10 + 12 + 83.3 % @@ -87,11 +87,11 @@
100.0%
100.0 % - 36 / 36 + 38 / 38 100.0 % 12 / 12 - 100.0 % - 8 / 8 + 83.3 % + 10 / 12 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleBlacklistBase.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleBlacklistBase.sol.func-sort-c.html index 3623044..0808b41 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleBlacklistBase.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleBlacklistBase.sol.func-sort-c.html @@ -31,13 +31,13 @@ lcov.info Lines: - 33 - 33 + 34 + 34 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 9 @@ -69,40 +69,40 @@ Hit count Sort by hit count - RuleBlacklistBase.transferred.1 - 2 + RuleBlacklistBase.canReturnTransferRestrictionCode + 4 - RuleBlacklistBase._transferredFrom - 4 + RuleBlacklistBase.messageForTransferRestriction + 12 - RuleBlacklistBase.canReturnTransferRestrictionCode - 4 + RuleBlacklistBase.transferred.0 + 18 - RuleBlacklistBase.messageForTransferRestriction - 12 + RuleBlacklistBase._transferred + 29 - RuleBlacklistBase._detectTransferRestrictionFrom - 22 + RuleBlacklistBase.transferred.1 + 46 - RuleBlacklistBase.transferred.0 - 58 + RuleBlacklistBase._transferredFrom + 54 - RuleBlacklistBase._transferred - 61 + RuleBlacklistBase.supportsInterface + 65 - RuleBlacklistBase.supportsInterface - 61 + RuleBlacklistBase._detectTransferRestrictionFrom + 80 - RuleBlacklistBase._detectTransferRestriction - 106 + RuleBlacklistBase._detectTransferRestriction + 140
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleBlacklistBase.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleBlacklistBase.sol.func.html index dddeb1b..57023b3 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleBlacklistBase.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleBlacklistBase.sol.func.html @@ -31,13 +31,13 @@ lcov.info Lines: - 33 - 33 + 34 + 34 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 9 @@ -69,40 +69,40 @@ Hit count Sort by hit count - RuleBlacklistBase._detectTransferRestriction - 106 + RuleBlacklistBase._detectTransferRestriction + 140 - RuleBlacklistBase._detectTransferRestrictionFrom - 22 + RuleBlacklistBase._detectTransferRestrictionFrom + 80 - RuleBlacklistBase._transferred - 61 + RuleBlacklistBase._transferred + 29 - RuleBlacklistBase._transferredFrom - 4 + RuleBlacklistBase._transferredFrom + 54 - RuleBlacklistBase.canReturnTransferRestrictionCode + RuleBlacklistBase.canReturnTransferRestrictionCode 4 - RuleBlacklistBase.messageForTransferRestriction + RuleBlacklistBase.messageForTransferRestriction 12 - RuleBlacklistBase.supportsInterface - 61 + RuleBlacklistBase.supportsInterface + 65 - RuleBlacklistBase.transferred.0 - 58 + RuleBlacklistBase.transferred.0 + 18 - RuleBlacklistBase.transferred.1 - 2 + RuleBlacklistBase.transferred.1 + 46
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleBlacklistBase.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleBlacklistBase.sol.gcov.html index 9df608c..fe77b99 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleBlacklistBase.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleBlacklistBase.sol.gcov.html @@ -31,13 +31,13 @@ lcov.info Lines: - 33 - 33 + 34 + 34 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 9 @@ -76,135 +76,179 @@ 5 : : import {RuleNFTAdapter} from "../core/RuleNFTAdapter.sol"; 6 : : import {RuleTransferValidation} from "../core/RuleTransferValidation.sol"; 7 : : import {RuleBlacklistInvariantStorage} from "../RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol"; - 8 : : import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; - 9 : : import {IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; - 10 : : import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; - 11 : : import {IRule} from "RuleEngine/interfaces/IRule.sol"; - 12 : : - 13 : : /** - 14 : : * @title RuleBlacklistBase - 15 : : * @notice Core blacklist logic without access-control policy. - 16 : : */ - 17 : : abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlacklistInvariantStorage { - 18 : : /*////////////////////////////////////////////////////////////// - 19 : : CONSTRUCTOR - 20 : : //////////////////////////////////////////////////////////////*/ - 21 : : - 22 : : constructor(address forwarderIrrevocable) RuleAddressSet(forwarderIrrevocable) {} - 23 : : - 24 : : /*////////////////////////////////////////////////////////////// - 25 : : PUBLIC FUNCTIONS - 26 : : //////////////////////////////////////////////////////////////*/ - 27 : : - 28 : : /** - 29 : : * @inheritdoc IERC3643IComplianceContract - 30 : : * @dev Validation only; does not modify state. - 31 : : */ - 32 : 58 : function transferred(address from, address to, uint256 value) - 33 : : public - 34 : : view - 35 : : virtual - 36 : : override(IERC3643IComplianceContract) - 37 : : { - 38 : 58 : _transferred(from, to, value); - 39 : : } - 40 : : - 41 : : /** - 42 : : * @inheritdoc IRuleEngine - 43 : : * @dev Validation only; does not modify state. - 44 : : */ - 45 : 2 : function transferred(address spender, address from, address to, uint256 value) - 46 : : public - 47 : : view - 48 : : virtual - 49 : : override(IRuleEngine) - 50 : : { - 51 : 2 : _transferredFrom(spender, from, to, value); - 52 : : } - 53 : : - 54 : 4 : function canReturnTransferRestrictionCode(uint8 restrictionCode) - 55 : : public - 56 : : pure - 57 : : virtual - 58 : : override(IRule) - 59 : : returns (bool) - 60 : : { - 61 : 4 : return restrictionCode == CODE_ADDRESS_FROM_IS_BLACKLISTED || restrictionCode == CODE_ADDRESS_TO_IS_BLACKLISTED - 62 : 1 : || restrictionCode == CODE_ADDRESS_SPENDER_IS_BLACKLISTED; - 63 : : } - 64 : : - 65 : 12 : function messageForTransferRestriction(uint8 restrictionCode) - 66 : : public - 67 : : pure - 68 : : virtual - 69 : : override(IERC1404) - 70 : : returns (string memory) - 71 : : { - 72 [ + + ]: 12 : if (restrictionCode == CODE_ADDRESS_FROM_IS_BLACKLISTED) { - 73 : 5 : return TEXT_ADDRESS_FROM_IS_BLACKLISTED; - 74 [ + + ]: 7 : } else if (restrictionCode == CODE_ADDRESS_TO_IS_BLACKLISTED) { - 75 : 3 : return TEXT_ADDRESS_TO_IS_BLACKLISTED; - 76 [ + + ]: 4 : } else if (restrictionCode == CODE_ADDRESS_SPENDER_IS_BLACKLISTED) { - 77 : 1 : return TEXT_ADDRESS_SPENDER_IS_BLACKLISTED; - 78 : : } else { - 79 : 3 : return TEXT_CODE_NOT_FOUND; - 80 : : } - 81 : : } - 82 : : - 83 : 61 : function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { - 84 : 61 : return RuleTransferValidation.supportsInterface(interfaceId); - 85 : : } - 86 : : - 87 : : /*////////////////////////////////////////////////////////////// - 88 : : INTERNAL FUNCTIONS - 89 : : //////////////////////////////////////////////////////////////*/ - 90 : : - 91 : 106 : function _detectTransferRestriction( - 92 : : address from, - 93 : : address to, - 94 : : uint256 /* value */ - 95 : : ) - 96 : : internal - 97 : : view - 98 : : override - 99 : : returns (uint8) - 100 : : { - 101 [ + + ]: 106 : if (isAddressListed(from)) { - 102 : 23 : return CODE_ADDRESS_FROM_IS_BLACKLISTED; - 103 [ + ]: 83 : } else if (isAddressListed(to)) { - 104 : 19 : return CODE_ADDRESS_TO_IS_BLACKLISTED; - 105 : : } - 106 : 64 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 107 : : } - 108 : : - 109 : 22 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - 110 : : internal - 111 : : view - 112 : : override - 113 : : returns (uint8) - 114 : : { - 115 [ + ]: 22 : if (isAddressListed(spender)) { - 116 : 8 : return CODE_ADDRESS_SPENDER_IS_BLACKLISTED; - 117 : : } - 118 : 14 : return _detectTransferRestriction(from, to, value); - 119 : : } - 120 : : - 121 : 61 : function _transferred(address from, address to, uint256 value) internal view virtual override { - 122 : 61 : uint8 code = _detectTransferRestriction(from, to, value); - 123 [ + + ]: 61 : require( - 124 : : code == uint8(REJECTED_CODE_BASE.TRANSFER_OK), - 125 : : RuleBlacklist_InvalidTransfer(address(this), from, to, value, code) - 126 : : ); - 127 : : } - 128 : : - 129 : 4 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { - 130 : 4 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); - 131 [ + + ]: 4 : require( - 132 : : code == uint8(REJECTED_CODE_BASE.TRANSFER_OK), - 133 : : RuleBlacklist_InvalidTransferFrom(address(this), spender, from, to, value, code) - 134 : : ); - 135 : : } - 136 : : } + 8 : : import {AddressListInterfaceId} from "../../../interfaces/library/AddressListInterfaceId.sol"; + 9 : : import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; + 10 : : import {IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; + 11 : : import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; + 12 : : import {IRule} from "RuleEngine/interfaces/IRule.sol"; + 13 : : + 14 : : /** + 15 : : * @title RuleBlacklistBase + 16 : : * @notice Core blacklist logic without access-control policy. + 17 : : */ + 18 : : abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlacklistInvariantStorage { + 19 : : /*////////////////////////////////////////////////////////////// + 20 : : CONSTRUCTOR + 21 : : //////////////////////////////////////////////////////////////*/ + 22 : : + 23 : : /** + 24 : : * @notice Deploys the blacklist rule base. + 25 : : * @param forwarderIrrevocable Trusted ERC-2771 forwarder address for meta-transactions. + 26 : : */ + 27 : : constructor(address forwarderIrrevocable) RuleAddressSet(forwarderIrrevocable) {} + 28 : : + 29 : : /*////////////////////////////////////////////////////////////// + 30 : : PUBLIC FUNCTIONS + 31 : : //////////////////////////////////////////////////////////////*/ + 32 : : + 33 : : /** + 34 : : * @inheritdoc IERC3643IComplianceContract + 35 : : * @dev Validation only; does not modify state. + 36 : : */ + 37 : 18 : function transferred(address from, address to, uint256 value) + 38 : : public + 39 : : view + 40 : : virtual + 41 : : override(IERC3643IComplianceContract) + 42 : : { + 43 : 18 : _transferred(from, to, value); + 44 : : } + 45 : : + 46 : : /** + 47 : : * @inheritdoc IRuleEngine + 48 : : * @dev Validation only; does not modify state. + 49 : : */ + 50 : 46 : function transferred(address spender, address from, address to, uint256 value) + 51 : : public + 52 : : view + 53 : : virtual + 54 : : override(IRuleEngine) + 55 : : { + 56 : 46 : _transferredFrom(spender, from, to, value); + 57 : : } + 58 : : + 59 : : /** + 60 : : * @inheritdoc IRule + 61 : : */ + 62 : 4 : function canReturnTransferRestrictionCode(uint8 restrictionCode) + 63 : : public + 64 : : pure + 65 : : virtual + 66 : : override(IRule) + 67 : : returns (bool) + 68 : : { + 69 : 4 : return restrictionCode == CODE_ADDRESS_FROM_IS_BLACKLISTED || restrictionCode == CODE_ADDRESS_TO_IS_BLACKLISTED + 70 : 1 : || restrictionCode == CODE_ADDRESS_SPENDER_IS_BLACKLISTED; + 71 : : } + 72 : : + 73 : : /** + 74 : : * @inheritdoc IERC1404 + 75 : : */ + 76 : 12 : function messageForTransferRestriction(uint8 restrictionCode) + 77 : : public + 78 : : pure + 79 : : virtual + 80 : : override(IERC1404) + 81 : : returns (string memory) + 82 : : { + 83 [ + + ]: 12 : if (restrictionCode == CODE_ADDRESS_FROM_IS_BLACKLISTED) { + 84 : 5 : return TEXT_ADDRESS_FROM_IS_BLACKLISTED; + 85 [ + + ]: 7 : } else if (restrictionCode == CODE_ADDRESS_TO_IS_BLACKLISTED) { + 86 : 3 : return TEXT_ADDRESS_TO_IS_BLACKLISTED; + 87 [ + + ]: 4 : } else if (restrictionCode == CODE_ADDRESS_SPENDER_IS_BLACKLISTED) { + 88 : 1 : return TEXT_ADDRESS_SPENDER_IS_BLACKLISTED; + 89 : : } else { + 90 : 3 : return TEXT_CODE_NOT_FOUND; + 91 : : } + 92 : : } + 93 : : + 94 : : /** + 95 : : * @inheritdoc RuleTransferValidation + 96 : : */ + 97 : 65 : function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { + 98 : : // Advertise IAddressList: this rule manages an address set and is callable through + 99 : : // the IAddressList interface. + 100 : 65 : return interfaceId == AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID + 101 : 63 : || RuleTransferValidation.supportsInterface(interfaceId); + 102 : : } + 103 : : + 104 : : /*////////////////////////////////////////////////////////////// + 105 : : INTERNAL FUNCTIONS + 106 : : //////////////////////////////////////////////////////////////*/ + 107 : : + 108 : : /** + 109 : : * @notice Detects whether a direct transfer is restricted by the blacklist. + 110 : : * @param from The sender address. + 111 : : * @param to The recipient address. + 112 : : * @return The restriction code, or TRANSFER_OK if neither party is blacklisted. + 113 : : */ + 114 : 140 : function _detectTransferRestriction( + 115 : : address from, + 116 : : address to, + 117 : : uint256 /* value */ + 118 : : ) + 119 : : internal + 120 : : view + 121 : : override + 122 : : returns (uint8) + 123 : : { + 124 [ + + ]: 140 : if (isAddressListed(from)) { + 125 : 40 : return CODE_ADDRESS_FROM_IS_BLACKLISTED; + 126 [ + ]: 100 : } else if (isAddressListed(to)) { + 127 : 19 : return CODE_ADDRESS_TO_IS_BLACKLISTED; + 128 : : } + 129 : 81 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 130 : : } + 131 : : + 132 : : /** + 133 : : * @notice Detects whether a delegated transfer is restricted by the blacklist. + 134 : : * @param spender The delegated spender address. + 135 : : * @param from The sender address. + 136 : : * @param to The recipient address. + 137 : : * @param value The amount transferred. + 138 : : * @return The restriction code, or TRANSFER_OK if no party is blacklisted. + 139 : : */ + 140 : 80 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + 141 : : internal + 142 : : view + 143 : : override + 144 : : returns (uint8) + 145 : : { + 146 [ + ]: 80 : if (isAddressListed(spender)) { + 147 : 8 : return CODE_ADDRESS_SPENDER_IS_BLACKLISTED; + 148 : : } + 149 : 72 : return _detectTransferRestriction(from, to, value); + 150 : : } + 151 : : + 152 : : /** + 153 : : * @notice Reverts if a direct transfer is blocked by the blacklist. + 154 : : * @param from The sender address. + 155 : : * @param to The recipient address. + 156 : : * @param value The amount transferred. + 157 : : */ + 158 : 29 : function _transferred(address from, address to, uint256 value) internal view virtual override { + 159 : 29 : uint8 code = _detectTransferRestriction(from, to, value); + 160 [ + + ]: 29 : require( + 161 : : code == uint8(REJECTED_CODE_BASE.TRANSFER_OK), + 162 : : RuleBlacklist_InvalidTransfer(address(this), from, to, value, code) + 163 : : ); + 164 : : } + 165 : : + 166 : : /** + 167 : : * @notice Reverts if a delegated transfer is blocked by the blacklist. + 168 : : * @param spender The delegated spender address. + 169 : : * @param from The sender address. + 170 : : * @param to The recipient address. + 171 : : * @param value The amount transferred. + 172 : : */ + 173 : 54 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { + 174 : 54 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); + 175 [ + + ]: 54 : require( + 176 : : code == uint8(REJECTED_CODE_BASE.TRANSFER_OK), + 177 : : RuleBlacklist_InvalidTransferFrom(address(this), spender, from, to, value, code) + 178 : : ); + 179 : : } + 180 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleERC2980Base.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleERC2980Base.sol.func-sort-c.html index bd5584f..5176cd9 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleERC2980Base.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleERC2980Base.sol.func-sort-c.html @@ -31,26 +31,26 @@ lcov.info Lines: - 105 - 109 - 96.3 % + 127 + 132 + 96.2 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 34 - 38 - 89.5 % + 37 + 42 + 88.1 % Branches: - 26 - 26 + 34 + 34 100.0 % @@ -69,156 +69,172 @@ Hit count Sort by hit count - RuleERC2980Base._authorizeFrozenlistAdd + RuleERC2980Base._authorizeFrozenlistAdd 0 - RuleERC2980Base._authorizeFrozenlistRemove + RuleERC2980Base._authorizeFrozenlistRemove 0 - RuleERC2980Base._authorizeWhitelistAdd + RuleERC2980Base._authorizeMintBurnManager 0 - RuleERC2980Base._authorizeWhitelistRemove + RuleERC2980Base._authorizeWhitelistAdd 0 - RuleERC2980Base.areFrozen - 1 + RuleERC2980Base._authorizeWhitelistRemove + 0 - RuleERC2980Base.areWhitelisted + RuleERC2980Base.areFrozen 1 - RuleERC2980Base.supportsInterface + RuleERC2980Base.areWhitelisted 1 - RuleERC2980Base._msgData - 2 - - - RuleERC2980Base.onlyFrozenlistRemove + RuleERC2980Base._msgData 2 - RuleERC2980Base.removeFrozenlistAddresses + RuleERC2980Base.onlyFrozenlistRemove 2 - RuleERC2980Base.transferred.1 + RuleERC2980Base.removeFrozenlistAddresses 2 - RuleERC2980Base._transferredFrom + RuleERC2980Base.setAllowBurn 3 - RuleERC2980Base.isVerified + RuleERC2980Base.supportsInterface 3 - RuleERC2980Base.frozenlist + RuleERC2980Base.frozenlistAddressCount 4 - RuleERC2980Base.frozenlistAddressCount + RuleERC2980Base.removeWhitelistAddresses 4 - RuleERC2980Base.removeWhitelistAddresses + RuleERC2980Base.transferred.1 4 - RuleERC2980Base.transferred.0 - 4 + RuleERC2980Base.canReturnTransferRestrictionCode + 5 - RuleERC2980Base._transferred + RuleERC2980Base.isVerified 5 - RuleERC2980Base.canReturnTransferRestrictionCode + RuleERC2980Base.onlyMintBurnManager 5 - RuleERC2980Base.messageForTransferRestriction + RuleERC2980Base.setAllowMint 5 - RuleERC2980Base.whitelistAddressCount + RuleERC2980Base.whitelistAddressCount 5 - RuleERC2980Base.addFrozenlistAddresses + RuleERC2980Base.addFrozenlistAddresses 6 - RuleERC2980Base.addWhitelistAddresses + RuleERC2980Base.addWhitelistAddresses 6 - RuleERC2980Base.onlyFrozenlistAdd + RuleERC2980Base.onlyFrozenlistAdd 6 - RuleERC2980Base.onlyWhitelistAdd + RuleERC2980Base.onlyWhitelistAdd 6 - RuleERC2980Base.onlyWhitelistRemove + RuleERC2980Base.transferred.0 + 6 + + + RuleERC2980Base.frozenlist + 7 + + + RuleERC2980Base.messageForTransferRestriction 7 - RuleERC2980Base.removeFrozenlistAddress + RuleERC2980Base.onlyWhitelistRemove 7 - RuleERC2980Base.removeWhitelistAddress + RuleERC2980Base.removeFrozenlistAddress 7 - RuleERC2980Base.whitelist + RuleERC2980Base.removeWhitelistAddress 7 - RuleERC2980Base._detectTransferRestrictionFrom - 8 + RuleERC2980Base._transferredFrom + 11 + + + RuleERC2980Base.whitelist + 11 - RuleERC2980Base.isFrozen + RuleERC2980Base.isFrozen 12 - RuleERC2980Base.isWhitelisted - 14 + RuleERC2980Base._transferred + 13 - RuleERC2980Base.addFrozenlistAddress - 22 + RuleERC2980Base.isWhitelisted + 15 - RuleERC2980Base._detectTransferRestriction + RuleERC2980Base._detectTransferRestrictionFrom 24 - RuleERC2980Base.addWhitelistAddress - 45 + RuleERC2980Base.addFrozenlistAddress + 24 + + + RuleERC2980Base.addWhitelistAddress + 49 + + + RuleERC2980Base._detectTransferRestriction + 62 - RuleERC2980Base.constructor - 67 + RuleERC2980Base.constructor + 75 - RuleERC2980Base._msgSender - 272 + RuleERC2980Base._msgSender + 293 - RuleERC2980Base._contextSuffixLength - 274 + RuleERC2980Base._contextSuffixLength + 295
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleERC2980Base.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleERC2980Base.sol.func.html index 6c2afc9..1105676 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleERC2980Base.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleERC2980Base.sol.func.html @@ -31,26 +31,26 @@ lcov.info Lines: - 105 - 109 - 96.3 % + 127 + 132 + 96.2 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 34 - 38 - 89.5 % + 37 + 42 + 88.1 % Branches: - 26 - 26 + 34 + 34 100.0 % @@ -69,155 +69,171 @@ Hit count Sort by hit count - RuleERC2980Base._authorizeFrozenlistAdd + RuleERC2980Base._authorizeFrozenlistAdd 0 - RuleERC2980Base._authorizeFrozenlistRemove + RuleERC2980Base._authorizeFrozenlistRemove 0 - RuleERC2980Base._authorizeWhitelistAdd + RuleERC2980Base._authorizeMintBurnManager 0 - RuleERC2980Base._authorizeWhitelistRemove + RuleERC2980Base._authorizeWhitelistAdd 0 - RuleERC2980Base._contextSuffixLength - 274 + RuleERC2980Base._authorizeWhitelistRemove + 0 - RuleERC2980Base._detectTransferRestriction - 24 + RuleERC2980Base._contextSuffixLength + 295 - RuleERC2980Base._detectTransferRestrictionFrom - 8 + RuleERC2980Base._detectTransferRestriction + 62 - RuleERC2980Base._msgData + RuleERC2980Base._detectTransferRestrictionFrom + 24 + + + RuleERC2980Base._msgData 2 - RuleERC2980Base._msgSender - 272 + RuleERC2980Base._msgSender + 293 - RuleERC2980Base._transferred - 5 + RuleERC2980Base._transferred + 13 - RuleERC2980Base._transferredFrom - 3 + RuleERC2980Base._transferredFrom + 11 - RuleERC2980Base.addFrozenlistAddress - 22 + RuleERC2980Base.addFrozenlistAddress + 24 - RuleERC2980Base.addFrozenlistAddresses + RuleERC2980Base.addFrozenlistAddresses 6 - RuleERC2980Base.addWhitelistAddress - 45 + RuleERC2980Base.addWhitelistAddress + 49 - RuleERC2980Base.addWhitelistAddresses + RuleERC2980Base.addWhitelistAddresses 6 - RuleERC2980Base.areFrozen + RuleERC2980Base.areFrozen 1 - RuleERC2980Base.areWhitelisted + RuleERC2980Base.areWhitelisted 1 - RuleERC2980Base.canReturnTransferRestrictionCode + RuleERC2980Base.canReturnTransferRestrictionCode 5 - RuleERC2980Base.constructor - 67 + RuleERC2980Base.constructor + 75 - RuleERC2980Base.frozenlist - 4 + RuleERC2980Base.frozenlist + 7 - RuleERC2980Base.frozenlistAddressCount + RuleERC2980Base.frozenlistAddressCount 4 - RuleERC2980Base.isFrozen + RuleERC2980Base.isFrozen 12 - RuleERC2980Base.isVerified - 3 + RuleERC2980Base.isVerified + 5 - RuleERC2980Base.isWhitelisted - 14 + RuleERC2980Base.isWhitelisted + 15 - RuleERC2980Base.messageForTransferRestriction - 5 + RuleERC2980Base.messageForTransferRestriction + 7 - RuleERC2980Base.onlyFrozenlistAdd + RuleERC2980Base.onlyFrozenlistAdd 6 - RuleERC2980Base.onlyFrozenlistRemove + RuleERC2980Base.onlyFrozenlistRemove 2 - RuleERC2980Base.onlyWhitelistAdd + RuleERC2980Base.onlyMintBurnManager + 5 + + + RuleERC2980Base.onlyWhitelistAdd 6 - RuleERC2980Base.onlyWhitelistRemove + RuleERC2980Base.onlyWhitelistRemove 7 - RuleERC2980Base.removeFrozenlistAddress + RuleERC2980Base.removeFrozenlistAddress 7 - RuleERC2980Base.removeFrozenlistAddresses + RuleERC2980Base.removeFrozenlistAddresses 2 - RuleERC2980Base.removeWhitelistAddress + RuleERC2980Base.removeWhitelistAddress 7 - RuleERC2980Base.removeWhitelistAddresses + RuleERC2980Base.removeWhitelistAddresses 4 - RuleERC2980Base.supportsInterface - 1 + RuleERC2980Base.setAllowBurn + 3 - RuleERC2980Base.transferred.0 - 4 + RuleERC2980Base.setAllowMint + 5 - RuleERC2980Base.transferred.1 - 2 + RuleERC2980Base.supportsInterface + 3 - RuleERC2980Base.whitelist - 7 + RuleERC2980Base.transferred.0 + 6 + + + RuleERC2980Base.transferred.1 + 4 + + + RuleERC2980Base.whitelist + 11 - RuleERC2980Base.whitelistAddressCount + RuleERC2980Base.whitelistAddressCount 5 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleERC2980Base.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleERC2980Base.sol.gcov.html index be5ba36..fca9ad0 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleERC2980Base.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleERC2980Base.sol.gcov.html @@ -31,26 +31,26 @@ lcov.info Lines: - 105 - 109 - 96.3 % + 127 + 132 + 96.2 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 34 - 38 - 89.5 % + 37 + 42 + 88.1 % Branches: - 26 - 26 + 34 + 34 100.0 % @@ -99,8 +99,8 @@ 28 : : */ 29 : : abstract contract RuleERC2980Base is 30 : : MetaTxModuleStandalone, - 31 : : RuleERC2980Internal, - 32 : : RuleERC2980InvariantStorage, + 31 : : RuleERC2980InvariantStorage, + 32 : : RuleERC2980Internal, 33 : : RuleNFTAdapter, 34 : : IERC2980, 35 : : IIdentityRegistryVerified @@ -109,339 +109,481 @@ 38 : : CONSTRUCTOR 39 : : //////////////////////////////////////////////////////////////*/ 40 : : - 41 : 67 : constructor(address forwarderIrrevocable, bool allowBurn) MetaTxModuleStandalone(forwarderIrrevocable) { - 42 [ + ]: 2 : if (allowBurn) { - 43 : 2 : _addWhitelistAddress(address(0)); - 44 : 2 : emit AddWhitelistAddress(address(0)); - 45 : : } - 46 : : } - 47 : : - 48 : : /*////////////////////////////////////////////////////////////// - 49 : : ACCESS CONTROL - 50 : : //////////////////////////////////////////////////////////////*/ - 51 : : - 52 : 6 : modifier onlyWhitelistAdd() { - 53 : 6 : _authorizeWhitelistAdd(); - 54 : : _; - 55 : : } - 56 : : - 57 : 7 : modifier onlyWhitelistRemove() { - 58 : 7 : _authorizeWhitelistRemove(); - 59 : : _; - 60 : : } - 61 : : - 62 : 6 : modifier onlyFrozenlistAdd() { - 63 : 6 : _authorizeFrozenlistAdd(); - 64 : : _; - 65 : : } - 66 : : - 67 : 2 : modifier onlyFrozenlistRemove() { - 68 : 2 : _authorizeFrozenlistRemove(); - 69 : : _; - 70 : : } - 71 : : - 72 : 0 : function _authorizeWhitelistAdd() internal view virtual; - 73 : 0 : function _authorizeWhitelistRemove() internal view virtual; - 74 : 0 : function _authorizeFrozenlistAdd() internal view virtual; - 75 : 0 : function _authorizeFrozenlistRemove() internal view virtual; - 76 : : - 77 : : /*////////////////////////////////////////////////////////////// - 78 : : WHITELIST MANAGEMENT - 79 : : //////////////////////////////////////////////////////////////*/ - 80 : : - 81 : : /** - 82 : : * @notice Adds multiple addresses to the whitelist. - 83 : : * @dev Does not revert if an address is already listed. - 84 : : */ - 85 : 6 : function addWhitelistAddresses(address[] calldata targetAddresses) public onlyWhitelistAdd { - 86 : 4 : _addWhitelistAddresses(targetAddresses); - 87 : 4 : emit AddWhitelistAddresses(targetAddresses); + 41 : : /** + 42 : : * @notice Whether this rule permits minting (`from == address(0)`). + 43 : : * @dev Mint/burn permission is an EXPLICIT flag, never whitelist membership of `address(0)`. + 44 : : * The zero address is the ERC-20 sentinel, not a participant: whitelisting it made the + 45 : : * MANDATORY ERC-2980 getter `whitelist(address(0))` return `true`, a spec violation. + 46 : : * A permitted mint still requires the RECIPIENT to be whitelisted and not frozen. + 47 : : */ + 48 : : bool public allowMint; + 49 : : + 50 : : /** + 51 : : * @notice Whether this rule permits burning (`to == address(0)`). + 52 : : * @dev See {allowMint}. A permitted burn still requires the SENDER not to be frozen. + 53 : : */ + 54 : : bool public allowBurn; + 55 : : + 56 : : /** + 57 : : * @notice Initializes the rule. + 58 : : * @dev `allowMintBurn` sets BOTH {allowMint} and {allowBurn} — the common case, since mint and + 59 : : * burn are normally permitted. Use {setAllowMint} / {setAllowBurn} afterwards for + 60 : : * independent control (e.g. to permanently close issuance while still allowing redemptions). + 61 : : * @param forwarderIrrevocable Trusted ERC-2771 forwarder address, set permanently at deployment. + 62 : : * @param allowMintBurn When true, permits both minting and burning. + 63 : : */ + 64 : 75 : constructor(address forwarderIrrevocable, bool allowMintBurn) MetaTxModuleStandalone(forwarderIrrevocable) { + 65 : 75 : allowMint = allowMintBurn; + 66 : 75 : allowBurn = allowMintBurn; + 67 : 75 : emit AllowMintUpdated(allowMintBurn); + 68 : 75 : emit AllowBurnUpdated(allowMintBurn); + 69 : : } + 70 : : + 71 : : /*////////////////////////////////////////////////////////////// + 72 : : ACCESS CONTROL + 73 : : //////////////////////////////////////////////////////////////*/ + 74 : : + 75 : 5 : modifier onlyMintBurnManager() { + 76 : 5 : _authorizeMintBurnManager(); + 77 : : _; + 78 : : } + 79 : : + 80 : 6 : modifier onlyWhitelistAdd() { + 81 : 6 : _authorizeWhitelistAdd(); + 82 : : _; + 83 : : } + 84 : : + 85 : 7 : modifier onlyWhitelistRemove() { + 86 : 7 : _authorizeWhitelistRemove(); + 87 : : _; 88 : : } 89 : : - 90 : : /** - 91 : : * @notice Removes multiple addresses from the whitelist. - 92 : : * @dev Does not revert if an address is not listed. - 93 : : */ - 94 : 4 : function removeWhitelistAddresses(address[] calldata targetAddresses) public onlyWhitelistRemove { - 95 : 3 : _removeWhitelistAddresses(targetAddresses); - 96 : 3 : emit RemoveWhitelistAddresses(targetAddresses); - 97 : : } - 98 : : - 99 : : /** - 100 : : * @notice Adds a single address to the whitelist. - 101 : : * @dev - 102 : : * Reverts if the address is already listed. - 103 : : * Deviation from ERC-2980 `Whitelistable` example interface: the spec's `addAddressToWhitelist` - 104 : : * returns `false` on duplicates instead of reverting. This implementation follows the codebase - 105 : : * convention of reverting on invalid single-item operations. - 106 : : */ - 107 : 45 : function addWhitelistAddress(address targetAddress) public onlyWhitelistAdd { - 108 [ + + ]: 42 : require(!_isWhitelisted(targetAddress), RuleERC2980_AddressAlreadyListed()); - 109 : 41 : _addWhitelistAddress(targetAddress); - 110 : 41 : emit AddWhitelistAddress(targetAddress); - 111 : : } - 112 : : - 113 : : /** - 114 : : * @notice Removes a single address from the whitelist. - 115 : : * @dev - 116 : : * Reverts if the address is not listed. - 117 : : * Deviation from ERC-2980 `Whitelistable` example interface: the spec's `removeAddressFromWhitelist` - 118 : : * returns `false` when not found instead of reverting. This implementation follows the codebase - 119 : : * convention of reverting on invalid single-item operations. - 120 : : */ - 121 : 7 : function removeWhitelistAddress(address targetAddress) public onlyWhitelistRemove { - 122 [ + + ]: 5 : require(_isWhitelisted(targetAddress), RuleERC2980_AddressNotFound()); - 123 : 4 : _removeWhitelistAddress(targetAddress); - 124 : 4 : emit RemoveWhitelistAddress(targetAddress); - 125 : : } - 126 : : - 127 : : /*////////////////////////////////////////////////////////////// - 128 : : FROZENLIST MANAGEMENT - 129 : : //////////////////////////////////////////////////////////////*/ - 130 : : - 131 : : /** - 132 : : * @notice Adds multiple addresses to the frozenlist. - 133 : : * @dev Does not revert if an address is already listed. - 134 : : */ - 135 : 6 : function addFrozenlistAddresses(address[] calldata targetAddresses) public onlyFrozenlistAdd { - 136 : 4 : _addFrozenlistAddresses(targetAddresses); - 137 : 4 : emit AddFrozenlistAddresses(targetAddresses); + 90 : 6 : modifier onlyFrozenlistAdd() { + 91 : 6 : _authorizeFrozenlistAdd(); + 92 : : _; + 93 : : } + 94 : : + 95 : 2 : modifier onlyFrozenlistRemove() { + 96 : 2 : _authorizeFrozenlistRemove(); + 97 : : _; + 98 : : } + 99 : : + 100 : : /*////////////////////////////////////////////////////////////// + 101 : : WHITELIST MANAGEMENT + 102 : : //////////////////////////////////////////////////////////////*/ + 103 : : + 104 : : /** + 105 : : * @notice Adds multiple addresses to the whitelist. + 106 : : * @dev Does not revert if an address is already listed. + 107 : : * @param targetAddresses Addresses to add to the whitelist. + 108 : : */ + 109 : 6 : function addWhitelistAddresses(address[] calldata targetAddresses) public onlyWhitelistAdd { + 110 : 4 : _addWhitelistAddresses(targetAddresses); + 111 : 4 : emit AddWhitelistAddresses(targetAddresses); + 112 : : } + 113 : : + 114 : : /** + 115 : : * @notice Removes multiple addresses from the whitelist. + 116 : : * @dev Does not revert if an address is not listed. + 117 : : * @param targetAddresses Addresses to remove from the whitelist. + 118 : : */ + 119 : 4 : function removeWhitelistAddresses(address[] calldata targetAddresses) public onlyWhitelistRemove { + 120 : 3 : _removeWhitelistAddresses(targetAddresses); + 121 : 3 : emit RemoveWhitelistAddresses(targetAddresses); + 122 : : } + 123 : : + 124 : : /** + 125 : : * @notice Adds a single address to the whitelist. + 126 : : * @dev + 127 : : * Reverts if the address is already listed. + 128 : : * Deviation from ERC-2980 `Whitelistable` example interface: the spec's `addAddressToWhitelist` + 129 : : * returns `false` on duplicates instead of reverting. This implementation follows the codebase + 130 : : * convention of reverting on invalid single-item operations. + 131 : : * @param targetAddress Address to add to the whitelist. + 132 : : */ + 133 : 49 : function addWhitelistAddress(address targetAddress) public onlyWhitelistAdd { + 134 [ + + ]: 46 : require(targetAddress != address(0), RuleERC2980_ZeroAddressNotAllowed()); + 135 [ + + ]: 45 : require(!_isWhitelisted(targetAddress), RuleERC2980_AddressAlreadyWhitelisted()); + 136 : 44 : _addWhitelistAddress(targetAddress); + 137 : 44 : emit AddWhitelistAddress(targetAddress); 138 : : } 139 : : 140 : : /** - 141 : : * @notice Removes multiple addresses from the frozenlist. - 142 : : * @dev Does not revert if an address is not listed. - 143 : : */ - 144 : 2 : function removeFrozenlistAddresses(address[] calldata targetAddresses) public onlyFrozenlistRemove { - 145 : 2 : _removeFrozenlistAddresses(targetAddresses); - 146 : 2 : emit RemoveFrozenlistAddresses(targetAddresses); - 147 : : } - 148 : : - 149 : : /** - 150 : : * @notice Adds a single address to the frozenlist. - 151 : : * @dev - 152 : : * Reverts if the address is already listed. - 153 : : * Deviation from ERC-2980 `Freezable` example interface: the spec's `addAddressToFrozenlist` - 154 : : * returns `false` on duplicates instead of reverting. This implementation follows the codebase - 155 : : * convention of reverting on invalid single-item operations. - 156 : : */ - 157 : 22 : function addFrozenlistAddress(address targetAddress) public onlyFrozenlistAdd { - 158 [ + + ]: 19 : require(!_isFrozen(targetAddress), RuleERC2980_AddressAlreadyListed()); - 159 : 18 : _addFrozenlistAddress(targetAddress); - 160 : 18 : emit AddFrozenlistAddress(targetAddress); - 161 : : } - 162 : : - 163 : : /** - 164 : : * @notice Removes a single address from the frozenlist. - 165 : : * @dev - 166 : : * Reverts if the address is not listed. - 167 : : * Deviation from ERC-2980 `Freezable` example interface: the spec's `removeAddressFromFrozenlist` - 168 : : * returns `false` when not found instead of reverting. This implementation follows the codebase - 169 : : * convention of reverting on invalid single-item operations. - 170 : : */ - 171 : 7 : function removeFrozenlistAddress(address targetAddress) public onlyFrozenlistRemove { - 172 [ + + ]: 5 : require(_isFrozen(targetAddress), RuleERC2980_AddressNotFound()); - 173 : 4 : _removeFrozenlistAddress(targetAddress); - 174 : 4 : emit RemoveFrozenlistAddress(targetAddress); - 175 : : } - 176 : : - 177 : : /*////////////////////////////////////////////////////////////// - 178 : : PUBLIC FUNCTIONS - 179 : : //////////////////////////////////////////////////////////////*/ - 180 : : - 181 : 4 : function transferred(address from, address to, uint256 value) - 182 : : public - 183 : : view - 184 : : virtual - 185 : : override(IERC3643IComplianceContract) - 186 : : { - 187 : 4 : _transferred(from, to, value); - 188 : : } - 189 : : - 190 : 2 : function transferred(address spender, address from, address to, uint256 value) - 191 : : public - 192 : : view - 193 : : virtual - 194 : : override(IRuleEngine) - 195 : : { - 196 : 2 : _transferredFrom(spender, from, to, value); - 197 : : } - 198 : : - 199 : 5 : function canReturnTransferRestrictionCode(uint8 restrictionCode) - 200 : : public - 201 : : pure - 202 : : virtual - 203 : : override(IRule) - 204 : : returns (bool) - 205 : : { - 206 : 5 : return restrictionCode == CODE_ADDRESS_FROM_IS_FROZEN || restrictionCode == CODE_ADDRESS_TO_IS_FROZEN - 207 : 3 : || restrictionCode == CODE_ADDRESS_SPENDER_IS_FROZEN || restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED; + 141 : : * @notice Removes a single address from the whitelist. + 142 : : * @dev + 143 : : * Reverts if the address is not listed. + 144 : : * Deviation from ERC-2980 `Whitelistable` example interface: the spec's `removeAddressFromWhitelist` + 145 : : * returns `false` when not found instead of reverting. This implementation follows the codebase + 146 : : * convention of reverting on invalid single-item operations. + 147 : : * @param targetAddress Address to remove from the whitelist. + 148 : : */ + 149 : 7 : function removeWhitelistAddress(address targetAddress) public onlyWhitelistRemove { + 150 [ + + ]: 5 : require(_isWhitelisted(targetAddress), RuleERC2980_AddressNotWhitelisted()); + 151 : 4 : _removeWhitelistAddress(targetAddress); + 152 : 4 : emit RemoveWhitelistAddress(targetAddress); + 153 : : } + 154 : : + 155 : : /*////////////////////////////////////////////////////////////// + 156 : : FROZENLIST MANAGEMENT + 157 : : //////////////////////////////////////////////////////////////*/ + 158 : : + 159 : : /** + 160 : : * @notice Adds multiple addresses to the frozenlist. + 161 : : * @dev Does not revert if an address is already listed. + 162 : : * @param targetAddresses Addresses to add to the frozenlist. + 163 : : */ + 164 : 6 : function addFrozenlistAddresses(address[] calldata targetAddresses) public onlyFrozenlistAdd { + 165 : 4 : _addFrozenlistAddresses(targetAddresses); + 166 : 4 : emit AddFrozenlistAddresses(targetAddresses); + 167 : : } + 168 : : + 169 : : /** + 170 : : * @notice Removes multiple addresses from the frozenlist. + 171 : : * @dev Does not revert if an address is not listed. + 172 : : * @param targetAddresses Addresses to remove from the frozenlist. + 173 : : */ + 174 : 2 : function removeFrozenlistAddresses(address[] calldata targetAddresses) public onlyFrozenlistRemove { + 175 : 2 : _removeFrozenlistAddresses(targetAddresses); + 176 : 2 : emit RemoveFrozenlistAddresses(targetAddresses); + 177 : : } + 178 : : + 179 : : /** + 180 : : * @notice Adds a single address to the frozenlist. + 181 : : * @dev + 182 : : * Reverts if the address is already listed. + 183 : : * Deviation from ERC-2980 `Freezable` example interface: the spec's `addAddressToFrozenlist` + 184 : : * returns `false` on duplicates instead of reverting. This implementation follows the codebase + 185 : : * convention of reverting on invalid single-item operations. + 186 : : * @param targetAddress Address to add to the frozenlist. + 187 : : */ + 188 : 24 : function addFrozenlistAddress(address targetAddress) public onlyFrozenlistAdd { + 189 [ + + ]: 21 : require(targetAddress != address(0), RuleERC2980_ZeroAddressNotAllowed()); + 190 [ + + ]: 20 : require(!_isFrozen(targetAddress), RuleERC2980_AddressAlreadyFrozen()); + 191 : 19 : _addFrozenlistAddress(targetAddress); + 192 : 19 : emit AddFrozenlistAddress(targetAddress); + 193 : : } + 194 : : + 195 : : /** + 196 : : * @notice Removes a single address from the frozenlist. + 197 : : * @dev + 198 : : * Reverts if the address is not listed. + 199 : : * Deviation from ERC-2980 `Freezable` example interface: the spec's `removeAddressFromFrozenlist` + 200 : : * returns `false` when not found instead of reverting. This implementation follows the codebase + 201 : : * convention of reverting on invalid single-item operations. + 202 : : * @param targetAddress Address to remove from the frozenlist. + 203 : : */ + 204 : 7 : function removeFrozenlistAddress(address targetAddress) public onlyFrozenlistRemove { + 205 [ + + ]: 5 : require(_isFrozen(targetAddress), RuleERC2980_AddressNotFrozen()); + 206 : 4 : _removeFrozenlistAddress(targetAddress); + 207 : 4 : emit RemoveFrozenlistAddress(targetAddress); 208 : : } 209 : : - 210 : 5 : function messageForTransferRestriction(uint8 restrictionCode) - 211 : : public - 212 : : pure - 213 : : virtual - 214 : : override(IERC1404) - 215 : : returns (string memory) - 216 : : { - 217 [ + + ]: 5 : if (restrictionCode == CODE_ADDRESS_FROM_IS_FROZEN) { - 218 : 1 : return TEXT_ADDRESS_FROM_IS_FROZEN; - 219 [ + + ]: 4 : } else if (restrictionCode == CODE_ADDRESS_TO_IS_FROZEN) { - 220 : 1 : return TEXT_ADDRESS_TO_IS_FROZEN; - 221 [ + + ]: 3 : } else if (restrictionCode == CODE_ADDRESS_SPENDER_IS_FROZEN) { - 222 : 1 : return TEXT_ADDRESS_SPENDER_IS_FROZEN; - 223 [ + + ]: 2 : } else if (restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED) { - 224 : 1 : return TEXT_ADDRESS_TO_NOT_WHITELISTED; - 225 : : } else { - 226 : 1 : return TEXT_CODE_NOT_FOUND; - 227 : : } - 228 : : } - 229 : : - 230 : 1 : function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { - 231 : 1 : return RuleTransferValidation.supportsInterface(interfaceId); - 232 : : } - 233 : : - 234 : : /** - 235 : : * @notice Returns the number of whitelisted addresses. - 236 : : */ - 237 : 5 : function whitelistAddressCount() public view returns (uint256) { - 238 : 5 : return _whitelistCount(); - 239 : : } - 240 : : - 241 : : /** - 242 : : * @notice Returns true if the address is in the whitelist. - 243 : : */ - 244 : 14 : function isWhitelisted(address targetAddress) public view returns (bool) { - 245 : 14 : return _isWhitelisted(targetAddress); - 246 : : } - 247 : : - 248 : : /** - 249 : : * @notice ERC-2980 getter: returns true if the address is whitelisted. - 250 : : */ - 251 : 7 : function whitelist(address _operator) public view virtual override(IERC2980) returns (bool) { - 252 : 7 : return _isWhitelisted(_operator); - 253 : : } - 254 : : - 255 : : /** - 256 : : * @notice Returns true if the address is whitelisted (identity-verified). - 257 : : * @dev Reflects whitelist membership only. Frozen status is intentionally excluded: - 258 : : * freezing is a temporary enforcement action and does not revoke identity verification. - 259 : : */ - 260 : 3 : function isVerified(address targetAddress) public view virtual override(IIdentityRegistryVerified) returns (bool) { - 261 : 3 : return _isWhitelisted(targetAddress); - 262 : : } - 263 : : - 264 : : /** - 265 : : * @notice Checks multiple addresses for whitelist membership. - 266 : : */ - 267 : 1 : function areWhitelisted(address[] memory targetAddresses) public view returns (bool[] memory results) { - 268 : 1 : results = new bool[](targetAddresses.length); - 269 : 1 : for (uint256 i = 0; i < targetAddresses.length; ++i) { - 270 : 2 : results[i] = _isWhitelisted(targetAddresses[i]); - 271 : : } - 272 : : } - 273 : : - 274 : : /** - 275 : : * @notice Returns the number of frozen addresses. - 276 : : */ - 277 : 4 : function frozenlistAddressCount() public view returns (uint256) { - 278 : 4 : return _frozenlistCount(); - 279 : : } - 280 : : - 281 : : /** - 282 : : * @notice Returns true if the address is in the frozenlist. - 283 : : */ - 284 : 12 : function isFrozen(address targetAddress) public view returns (bool) { - 285 : 12 : return _isFrozen(targetAddress); - 286 : : } - 287 : : - 288 : : /** - 289 : : * @notice ERC-2980 getter: returns true if the address is frozen. - 290 : : */ - 291 : 4 : function frozenlist(address _operator) public view virtual override(IERC2980) returns (bool) { - 292 : 4 : return _isFrozen(_operator); - 293 : : } - 294 : : - 295 : : /** - 296 : : * @notice Checks multiple addresses for frozenlist membership. - 297 : : */ - 298 : 1 : function areFrozen(address[] memory targetAddresses) public view returns (bool[] memory results) { - 299 : 1 : results = new bool[](targetAddresses.length); - 300 : 1 : for (uint256 i = 0; i < targetAddresses.length; ++i) { - 301 : 2 : results[i] = _isFrozen(targetAddresses[i]); - 302 : : } + 210 : : /*////////////////////////////////////////////////////////////// + 211 : : PUBLIC FUNCTIONS + 212 : : //////////////////////////////////////////////////////////////*/ + 213 : : + 214 : : /** + 215 : : * @notice Enables or disables minting through this rule. + 216 : : * @param value The new value of the `allowMint` flag. + 217 : : */ + 218 : 5 : function setAllowMint(bool value) public virtual onlyMintBurnManager { + 219 : 3 : allowMint = value; + 220 : 3 : emit AllowMintUpdated(value); + 221 : : } + 222 : : + 223 : : /** + 224 : : * @notice Enables or disables burning through this rule. + 225 : : * @param value The new value of the `allowBurn` flag. + 226 : : */ + 227 : 3 : function setAllowBurn(bool value) public virtual onlyMintBurnManager { + 228 : 2 : allowBurn = value; + 229 : 2 : emit AllowBurnUpdated(value); + 230 : : } + 231 : : + 232 : : /** + 233 : : * @inheritdoc IERC3643IComplianceContract + 234 : : */ + 235 : 6 : function transferred(address from, address to, uint256 value) + 236 : : public + 237 : : view + 238 : : virtual + 239 : : override(IERC3643IComplianceContract) + 240 : : { + 241 : 6 : _transferred(from, to, value); + 242 : : } + 243 : : + 244 : : /** + 245 : : * @inheritdoc IRuleEngine + 246 : : */ + 247 : 4 : function transferred(address spender, address from, address to, uint256 value) + 248 : : public + 249 : : view + 250 : : virtual + 251 : : override(IRuleEngine) + 252 : : { + 253 : 4 : _transferredFrom(spender, from, to, value); + 254 : : } + 255 : : + 256 : : /** + 257 : : * @inheritdoc IRule + 258 : : */ + 259 : 5 : function canReturnTransferRestrictionCode(uint8 restrictionCode) + 260 : : public + 261 : : pure + 262 : : virtual + 263 : : override(IRule) + 264 : : returns (bool) + 265 : : { + 266 : 5 : return restrictionCode == CODE_ADDRESS_FROM_IS_FROZEN || restrictionCode == CODE_ADDRESS_TO_IS_FROZEN + 267 : 3 : || restrictionCode == CODE_ADDRESS_SPENDER_IS_FROZEN || restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED + 268 : 1 : || restrictionCode == CODE_MINT_NOT_ALLOWED || restrictionCode == CODE_BURN_NOT_ALLOWED; + 269 : : } + 270 : : + 271 : : /** + 272 : : * @inheritdoc IERC1404 + 273 : : */ + 274 : 7 : function messageForTransferRestriction(uint8 restrictionCode) + 275 : : public + 276 : : pure + 277 : : virtual + 278 : : override(IERC1404) + 279 : : returns (string memory) + 280 : : { + 281 [ + + ]: 7 : if (restrictionCode == CODE_ADDRESS_FROM_IS_FROZEN) { + 282 : 1 : return TEXT_ADDRESS_FROM_IS_FROZEN; + 283 [ + + ]: 6 : } else if (restrictionCode == CODE_ADDRESS_TO_IS_FROZEN) { + 284 : 1 : return TEXT_ADDRESS_TO_IS_FROZEN; + 285 [ + + ]: 5 : } else if (restrictionCode == CODE_ADDRESS_SPENDER_IS_FROZEN) { + 286 : 1 : return TEXT_ADDRESS_SPENDER_IS_FROZEN; + 287 [ + + ]: 4 : } else if (restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED) { + 288 : 1 : return TEXT_ADDRESS_TO_NOT_WHITELISTED; + 289 [ + + ]: 3 : } else if (restrictionCode == CODE_MINT_NOT_ALLOWED) { + 290 : 1 : return TEXT_MINT_NOT_ALLOWED; + 291 [ + + ]: 2 : } else if (restrictionCode == CODE_BURN_NOT_ALLOWED) { + 292 : 1 : return TEXT_BURN_NOT_ALLOWED; + 293 : : } else { + 294 : 1 : return TEXT_CODE_NOT_FOUND; + 295 : : } + 296 : : } + 297 : : + 298 : : /** + 299 : : * @inheritdoc RuleTransferValidation + 300 : : */ + 301 : 3 : function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { + 302 : 3 : return RuleTransferValidation.supportsInterface(interfaceId); 303 : : } 304 : : - 305 : : /*////////////////////////////////////////////////////////////// - 306 : : INTERNAL FUNCTIONS - 307 : : //////////////////////////////////////////////////////////////*/ - 308 : : - 309 : 24 : function _detectTransferRestriction( - 310 : : address from, - 311 : : address to, - 312 : : uint256 /* value */ - 313 : : ) - 314 : : internal - 315 : : view - 316 : : virtual - 317 : : override - 318 : : returns (uint8) - 319 : : { - 320 : : // Frozenlist check has priority - 321 [ + + ]: 24 : if (_isFrozen(from)) { - 322 : 4 : return CODE_ADDRESS_FROM_IS_FROZEN; - 323 [ + ]: 20 : } else if (_isFrozen(to)) { - 324 : 4 : return CODE_ADDRESS_TO_IS_FROZEN; - 325 : : } - 326 : : // Whitelist check: only the recipient must be whitelisted - 327 [ + ]: 16 : if (!_isWhitelisted(to)) { - 328 : 5 : return CODE_ADDRESS_TO_NOT_WHITELISTED; - 329 : : } - 330 : 11 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 331 : : } - 332 : : - 333 : 8 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - 334 : : internal - 335 : : view - 336 : : virtual - 337 : : override - 338 : : returns (uint8) - 339 : : { - 340 [ + ]: 8 : if (_isFrozen(spender)) { - 341 : 4 : return CODE_ADDRESS_SPENDER_IS_FROZEN; - 342 : : } - 343 : 4 : return _detectTransferRestriction(from, to, value); - 344 : : } - 345 : : - 346 : 5 : function _transferred(address from, address to, uint256 value) internal view virtual override { - 347 : 5 : uint8 code = _detectTransferRestriction(from, to, value); - 348 [ + + ]: 5 : require( - 349 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), - 350 : : RuleERC2980_InvalidTransfer(address(this), from, to, value, code) - 351 : : ); + 305 : : /** + 306 : : * @notice Returns the number of whitelisted addresses. + 307 : : * @return The count of addresses currently in the whitelist. + 308 : : */ + 309 : 5 : function whitelistAddressCount() public view returns (uint256) { + 310 : 5 : return _whitelistCount(); + 311 : : } + 312 : : + 313 : : /** + 314 : : * @notice Returns true if the address is in the whitelist. + 315 : : * @param targetAddress Address to check. + 316 : : * @return True if the address is whitelisted. + 317 : : */ + 318 : 15 : function isWhitelisted(address targetAddress) public view returns (bool) { + 319 : 15 : return _isWhitelisted(targetAddress); + 320 : : } + 321 : : + 322 : : /** + 323 : : * @notice ERC-2980 getter: returns true if the address is whitelisted. + 324 : : * @param _operator Address to check. + 325 : : * @return True if the address is whitelisted. + 326 : : */ + 327 : 11 : function whitelist(address _operator) public view virtual override(IERC2980) returns (bool) { + 328 : 11 : return _isWhitelisted(_operator); + 329 : : } + 330 : : + 331 : : /** + 332 : : * @notice Returns true if the address is whitelisted (identity-verified). + 333 : : * @dev Reflects whitelist membership only. Frozen status is intentionally excluded: + 334 : : * freezing is a temporary enforcement action and does not revoke identity verification. + 335 : : * @param targetAddress Address to check. + 336 : : * @return True if the address is whitelisted. + 337 : : */ + 338 : 5 : function isVerified(address targetAddress) public view virtual override(IIdentityRegistryVerified) returns (bool) { + 339 : 5 : return _isWhitelisted(targetAddress); + 340 : : } + 341 : : + 342 : : /** + 343 : : * @notice Checks multiple addresses for whitelist membership. + 344 : : * @param targetAddresses Addresses to check. + 345 : : * @return results Array of booleans, true where the corresponding address is whitelisted. + 346 : : */ + 347 : 1 : function areWhitelisted(address[] memory targetAddresses) public view returns (bool[] memory results) { + 348 : 1 : results = new bool[](targetAddresses.length); + 349 : 1 : for (uint256 i = 0; i < targetAddresses.length; ++i) { + 350 : 2 : results[i] = _isWhitelisted(targetAddresses[i]); + 351 : : } 352 : : } 353 : : - 354 : 3 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { - 355 : 3 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); - 356 [ + + ]: 3 : require( - 357 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), - 358 : : RuleERC2980_InvalidTransferFrom(address(this), spender, from, to, value, code) - 359 : : ); + 354 : : /** + 355 : : * @notice Returns the number of frozen addresses. + 356 : : * @return The count of addresses currently in the frozenlist. + 357 : : */ + 358 : 4 : function frozenlistAddressCount() public view returns (uint256) { + 359 : 4 : return _frozenlistCount(); 360 : : } 361 : : - 362 : 272 : function _msgSender() internal view virtual override(ERC2771Context) returns (address sender) { - 363 : 272 : return ERC2771Context._msgSender(); - 364 : : } - 365 : : - 366 : 2 : function _msgData() internal view virtual override(ERC2771Context) returns (bytes calldata) { - 367 : 2 : return ERC2771Context._msgData(); - 368 : : } - 369 : : - 370 : 274 : function _contextSuffixLength() internal view virtual override(ERC2771Context) returns (uint256) { - 371 : 274 : return ERC2771Context._contextSuffixLength(); - 372 : : } - 373 : : } + 362 : : /** + 363 : : * @notice Returns true if the address is in the frozenlist. + 364 : : * @param targetAddress Address to check. + 365 : : * @return True if the address is frozen. + 366 : : */ + 367 : 12 : function isFrozen(address targetAddress) public view returns (bool) { + 368 : 12 : return _isFrozen(targetAddress); + 369 : : } + 370 : : + 371 : : /** + 372 : : * @notice ERC-2980 getter: returns true if the address is frozen. + 373 : : * @param _operator Address to check. + 374 : : * @return True if the address is frozen. + 375 : : */ + 376 : 7 : function frozenlist(address _operator) public view virtual override(IERC2980) returns (bool) { + 377 : 7 : return _isFrozen(_operator); + 378 : : } + 379 : : + 380 : : /** + 381 : : * @notice Checks multiple addresses for frozenlist membership. + 382 : : * @param targetAddresses Addresses to check. + 383 : : * @return results Array of booleans, true where the corresponding address is frozen. + 384 : : */ + 385 : 1 : function areFrozen(address[] memory targetAddresses) public view returns (bool[] memory results) { + 386 : 1 : results = new bool[](targetAddresses.length); + 387 : 1 : for (uint256 i = 0; i < targetAddresses.length; ++i) { + 388 : 2 : results[i] = _isFrozen(targetAddresses[i]); + 389 : : } + 390 : : } + 391 : : + 392 : : /*////////////////////////////////////////////////////////////// + 393 : : INTERNAL FUNCTIONS + 394 : : //////////////////////////////////////////////////////////////*/ + 395 : : + 396 : : /** + 397 : : * @notice Authorization hook invoked before toggling `allowMint` / `allowBurn`. + 398 : : */ + 399 : 0 : function _authorizeMintBurnManager() internal view virtual; + 400 : : + 401 : : /** + 402 : : * @notice Authorization hook invoked before adding addresses to the whitelist. + 403 : : */ + 404 : 0 : function _authorizeWhitelistAdd() internal view virtual; + 405 : : /** + 406 : : * @notice Authorization hook invoked before removing addresses from the whitelist. + 407 : : */ + 408 : 0 : function _authorizeWhitelistRemove() internal view virtual; + 409 : : /** + 410 : : * @notice Authorization hook invoked before adding addresses to the frozenlist. + 411 : : */ + 412 : 0 : function _authorizeFrozenlistAdd() internal view virtual; + 413 : : /** + 414 : : * @notice Authorization hook invoked before removing addresses from the frozenlist. + 415 : : */ + 416 : 0 : function _authorizeFrozenlistRemove() internal view virtual; + 417 : : + 418 : : /** + 419 : : * @inheritdoc RuleTransferValidation + 420 : : */ + 421 : 62 : function _detectTransferRestriction( + 422 : : address from, + 423 : : address to, + 424 : : uint256 /* value */ + 425 : : ) + 426 : : internal + 427 : : view + 428 : : virtual + 429 : : override + 430 : : returns (uint8) + 431 : : { + 432 : 62 : bool isMint = from == address(0); + 433 : 62 : bool isBurn = to == address(0); + 434 : : + 435 : : // Gate the mint/burn OPERATION explicitly, rather than by whitelisting the zero address. + 436 [ + ]: 62 : if (isMint && !allowMint) { + 437 : 1 : return CODE_MINT_NOT_ALLOWED; + 438 : : } + 439 [ + ]: 61 : if (isBurn && !allowBurn) { + 440 : 2 : return CODE_BURN_NOT_ALLOWED; + 441 : : } + 442 : : + 443 : : // Frozenlist check has priority — but only for REAL participants. + 444 [ + ]: 59 : if (!isMint && _isFrozen(from)) { + 445 : 20 : return CODE_ADDRESS_FROM_IS_FROZEN; + 446 : : } + 447 [ + ]: 39 : if (!isBurn && _isFrozen(to)) { + 448 : 4 : return CODE_ADDRESS_TO_IS_FROZEN; + 449 : : } + 450 : : // Whitelist check: only the recipient must be whitelisted (ERC-2980); no recipient on a burn. + 451 [ + ]: 35 : if (!isBurn && !_isWhitelisted(to)) { + 452 : 5 : return CODE_ADDRESS_TO_NOT_WHITELISTED; + 453 : : } + 454 : 30 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 455 : : } + 456 : : + 457 : : /** + 458 : : * @inheritdoc RuleTransferValidation + 459 : : */ + 460 : 24 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + 461 : : internal + 462 : : view + 463 : : virtual + 464 : : override + 465 : : returns (uint8) + 466 : : { + 467 [ + ]: 24 : if (_isFrozen(spender)) { + 468 : 4 : return CODE_ADDRESS_SPENDER_IS_FROZEN; + 469 : : } + 470 : 20 : return _detectTransferRestriction(from, to, value); + 471 : : } + 472 : : + 473 : : /** + 474 : : * @inheritdoc RuleNFTAdapter + 475 : : */ + 476 : 13 : function _transferred(address from, address to, uint256 value) internal view virtual override { + 477 : 13 : uint8 code = _detectTransferRestriction(from, to, value); + 478 [ + + ]: 13 : require( + 479 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), + 480 : : RuleERC2980_InvalidTransfer(address(this), from, to, value, code) + 481 : : ); + 482 : : } + 483 : : + 484 : : /** + 485 : : * @inheritdoc RuleNFTAdapter + 486 : : */ + 487 : 11 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { + 488 : 11 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); + 489 [ + + ]: 11 : require( + 490 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), + 491 : : RuleERC2980_InvalidTransferFrom(address(this), spender, from, to, value, code) + 492 : : ); + 493 : : } + 494 : : + 495 : : /** + 496 : : * @inheritdoc ERC2771Context + 497 : : */ + 498 : 293 : function _msgSender() internal view virtual override(ERC2771Context) returns (address sender) { + 499 : 293 : return ERC2771Context._msgSender(); + 500 : : } + 501 : : + 502 : : /** + 503 : : * @inheritdoc ERC2771Context + 504 : : */ + 505 : 2 : function _msgData() internal view virtual override(ERC2771Context) returns (bytes calldata) { + 506 : 2 : return ERC2771Context._msgData(); + 507 : : } + 508 : : + 509 : : /** + 510 : : * @inheritdoc ERC2771Context + 511 : : */ + 512 : 295 : function _contextSuffixLength() internal view virtual override(ERC2771Context) returns (uint256) { + 513 : 295 : return ERC2771Context._contextSuffixLength(); + 514 : : } + 515 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol.func-sort-c.html index 0798224..1cee4d7 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol.func-sort-c.html @@ -31,18 +31,18 @@ lcov.info Lines: - 51 - 52 - 98.1 % + 63 + 64 + 98.4 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 12 - 13 - 92.3 % + 14 + 15 + 93.3 % @@ -69,56 +69,64 @@ Hit count Sort by hit count - RuleIdentityRegistryBase._authorizeIdentityRegistryManager + RuleIdentityRegistryBase._authorizeIdentityRegistryManager 0 - RuleIdentityRegistryBase._transferred + RuleIdentityRegistryBase.setCheckSender 2 - RuleIdentityRegistryBase._transferredFrom - 2 - - - RuleIdentityRegistryBase.transferred.0 - 2 + RuleIdentityRegistryBase.canReturnTransferRestrictionCode + 4 - RuleIdentityRegistryBase.transferred.1 - 2 + RuleIdentityRegistryBase.messageForTransferRestriction + 4 - RuleIdentityRegistryBase.canReturnTransferRestrictionCode + RuleIdentityRegistryBase.setIdentityRegistry 4 - RuleIdentityRegistryBase.messageForTransferRestriction - 4 + RuleIdentityRegistryBase.clearIdentityRegistry + 5 - RuleIdentityRegistryBase.setIdentityRegistry - 4 + RuleIdentityRegistryBase.onlyIdentityRegistryManager + 5 - RuleIdentityRegistryBase.clearIdentityRegistry + RuleIdentityRegistryBase.setCheckSpender 5 - RuleIdentityRegistryBase.onlyIdentityRegistryManager + RuleIdentityRegistryBase.transferred.0 5 - RuleIdentityRegistryBase._detectTransferRestrictionFrom - 9 + RuleIdentityRegistryBase.transferred.1 + 7 + + + RuleIdentityRegistryBase._transferred + 11 + + + RuleIdentityRegistryBase._transferredFrom + 13 + + + RuleIdentityRegistryBase._detectTransferRestrictionFrom + 31 - RuleIdentityRegistryBase._detectTransferRestriction - 16 + RuleIdentityRegistryBase.constructor + 39 - RuleIdentityRegistryBase.constructor - 26 + RuleIdentityRegistryBase._detectTransferRestriction + 62
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol.func.html index 36cb85d..c8835b6 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol.func.html @@ -31,18 +31,18 @@ lcov.info Lines: - 51 - 52 - 98.1 % + 63 + 64 + 98.4 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 12 - 13 - 92.3 % + 14 + 15 + 93.3 % @@ -69,56 +69,64 @@ Hit count Sort by hit count - RuleIdentityRegistryBase._authorizeIdentityRegistryManager + RuleIdentityRegistryBase._authorizeIdentityRegistryManager 0 - RuleIdentityRegistryBase._detectTransferRestriction - 16 + RuleIdentityRegistryBase._detectTransferRestriction + 62 - RuleIdentityRegistryBase._detectTransferRestrictionFrom - 9 + RuleIdentityRegistryBase._detectTransferRestrictionFrom + 31 - RuleIdentityRegistryBase._transferred - 2 + RuleIdentityRegistryBase._transferred + 11 - RuleIdentityRegistryBase._transferredFrom - 2 + RuleIdentityRegistryBase._transferredFrom + 13 - RuleIdentityRegistryBase.canReturnTransferRestrictionCode + RuleIdentityRegistryBase.canReturnTransferRestrictionCode 4 - RuleIdentityRegistryBase.clearIdentityRegistry + RuleIdentityRegistryBase.clearIdentityRegistry 5 - RuleIdentityRegistryBase.constructor - 26 + RuleIdentityRegistryBase.constructor + 39 - RuleIdentityRegistryBase.messageForTransferRestriction + RuleIdentityRegistryBase.messageForTransferRestriction 4 - RuleIdentityRegistryBase.onlyIdentityRegistryManager + RuleIdentityRegistryBase.onlyIdentityRegistryManager 5 - RuleIdentityRegistryBase.setIdentityRegistry + RuleIdentityRegistryBase.setCheckSender + 2 + + + RuleIdentityRegistryBase.setCheckSpender + 5 + + + RuleIdentityRegistryBase.setIdentityRegistry 4 - RuleIdentityRegistryBase.transferred.0 - 2 + RuleIdentityRegistryBase.transferred.0 + 5 - RuleIdentityRegistryBase.transferred.1 - 2 + RuleIdentityRegistryBase.transferred.1 + 7
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol.gcov.html index a9bd14c..99639cd 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol.gcov.html @@ -31,18 +31,18 @@ lcov.info Lines: - 51 - 52 - 98.1 % + 63 + 64 + 98.4 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 12 - 13 - 92.3 % + 14 + 15 + 93.3 % @@ -82,145 +82,267 @@ 11 : : /** 12 : : * @title RuleIdentityRegistryBase 13 : : * @notice Checks the ERC-3643 Identity Registry for transfer participants when configured. - 14 : : * @dev Burns (to == address(0)) are allowed even if the sender is not verified. - 15 : : */ - 16 : : abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegistryInvariantStorage { - 17 : : IIdentityRegistryVerified public identityRegistry; - 18 : : - 19 : : /*////////////////////////////////////////////////////////////// - 20 : : CONSTRUCTOR - 21 : : //////////////////////////////////////////////////////////////*/ - 22 : : - 23 : 26 : constructor(address identityRegistry_) { - 24 [ + ]: 26 : if (identityRegistry_ != address(0)) { - 25 : 25 : identityRegistry = IIdentityRegistryVerified(identityRegistry_); - 26 : : } - 27 : : } - 28 : : - 29 : : /*////////////////////////////////////////////////////////////// - 30 : : ACCESS CONTROL - 31 : : //////////////////////////////////////////////////////////////*/ - 32 : : - 33 : 5 : modifier onlyIdentityRegistryManager() { - 34 : 5 : _authorizeIdentityRegistryManager(); - 35 : : _; - 36 : : } - 37 : : - 38 : 0 : function _authorizeIdentityRegistryManager() internal view virtual; - 39 : : - 40 : : /*////////////////////////////////////////////////////////////// - 41 : : EXTERNAL FUNCTIONS - 42 : : //////////////////////////////////////////////////////////////*/ + 14 : : * @dev **ERC-3643 conformant by default.** The specification mandates that ONLY THE RECEIVER be + 15 : : * identity-verified: + 16 : : * + 17 : : * - "The receiver MUST be whitelisted on the Identity Registry and verified" (§ Transfer) + 18 : : * - "`transferFrom` works the same way" (§ Transfer) + 19 : : * - "`mint` and `forcedTransfer` only require the receiver to be whitelisted + 20 : : * and verified on the Identity Registry" (§ Transfer) + 21 : : * - "The `burn` function bypasses all checks on eligibility" (§ Transfer) + 22 : : * + 23 : : * The sender, the spender and the minter are NOT required to be verified. Checking the sender + 24 : : * in particular would TRAP DE-LISTED HOLDERS: ERC-3643 screens only the receiver precisely so + 25 : : * that an investor whose identity lapses (expired claim, revoked identity) can still exit their + 26 : : * position by sending to a verified counterparty. + 27 : : * + 28 : : * Stricter screening remains available, but as an EXPLICIT OPT-IN ({checkSender}, + 29 : : * {checkSpender}) rather than an undocumented default. + 30 : : */ + 31 : : abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegistryInvariantStorage { + 32 : : /** + 33 : : * @notice The ERC-3643 Identity Registry consulted to verify transfer participants; the zero address disables checks. + 34 : : */ + 35 : : IIdentityRegistryVerified public identityRegistry; + 36 : : + 37 : : /** + 38 : : * @notice When true, ALSO require the sender to be identity-verified. + 39 : : * @dev Defaults to FALSE: ERC-3643 does not require it. Enabling it is stricter than the + 40 : : * specification and prevents a de-listed holder from exiting their position. + 41 : : */ + 42 : : bool public checkSender; 43 : : - 44 : 4 : function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { - 45 : 4 : return restrictionCode == CODE_ADDRESS_FROM_NOT_VERIFIED || restrictionCode == CODE_ADDRESS_TO_NOT_VERIFIED - 46 : 2 : || restrictionCode == CODE_ADDRESS_SPENDER_NOT_VERIFIED; - 47 : : } - 48 : : - 49 : : /*////////////////////////////////////////////////////////////// - 50 : : PUBLIC FUNCTIONS - 51 : : //////////////////////////////////////////////////////////////*/ - 52 : : - 53 : 4 : function setIdentityRegistry(address newRegistry) public onlyIdentityRegistryManager { - 54 [ + + ]: 2 : require(newRegistry != address(0), RuleIdentityRegistry_RegistryAddressZeroNotAllowed()); - 55 : 1 : identityRegistry = IIdentityRegistryVerified(newRegistry); - 56 : 1 : emit IdentityRegistryUpdated(newRegistry); - 57 : : } - 58 : : - 59 : 5 : function clearIdentityRegistry() public onlyIdentityRegistryManager { - 60 : 3 : identityRegistry = IIdentityRegistryVerified(address(0)); - 61 : 3 : emit IdentityRegistryUpdated(address(0)); - 62 : : } - 63 : : - 64 : 2 : function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { - 65 : 2 : _transferred(from, to, value); - 66 : : } - 67 : : - 68 : 2 : function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { - 69 : 2 : _transferredFrom(spender, from, to, value); - 70 : : } - 71 : : - 72 : 4 : function messageForTransferRestriction(uint8 restrictionCode) - 73 : : public - 74 : : pure - 75 : : override(IERC1404) - 76 : : returns (string memory) - 77 : : { - 78 [ + + ]: 4 : if (restrictionCode == CODE_ADDRESS_FROM_NOT_VERIFIED) { - 79 : 1 : return TEXT_ADDRESS_FROM_NOT_VERIFIED; - 80 [ + + ]: 3 : } else if (restrictionCode == CODE_ADDRESS_TO_NOT_VERIFIED) { - 81 : 1 : return TEXT_ADDRESS_TO_NOT_VERIFIED; - 82 [ + ]: 2 : } else if (restrictionCode == CODE_ADDRESS_SPENDER_NOT_VERIFIED) { - 83 : 1 : return TEXT_ADDRESS_SPENDER_NOT_VERIFIED; - 84 : : } - 85 : 1 : return TEXT_CODE_NOT_FOUND; - 86 : : } - 87 : : - 88 : : /*////////////////////////////////////////////////////////////// - 89 : : INTERNAL FUNCTIONS - 90 : : //////////////////////////////////////////////////////////////*/ - 91 : : - 92 : 16 : function _detectTransferRestriction( - 93 : : address from, - 94 : : address to, - 95 : : uint256 /* value */ - 96 : : ) - 97 : : internal - 98 : : view - 99 : : override - 100 : : returns (uint8) - 101 : : { - 102 [ + ]: 16 : if (address(identityRegistry) == address(0)) { - 103 : 3 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 104 : : } - 105 [ + ]: 13 : if (to == address(0)) { - 106 : 2 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 107 : : } - 108 : : - 109 [ + ]: 11 : if (from != address(0) && !identityRegistry.isVerified(from)) { - 110 : 4 : return CODE_ADDRESS_FROM_NOT_VERIFIED; - 111 : : } - 112 [ + ]: 7 : if (to != address(0) && !identityRegistry.isVerified(to)) { - 113 : 1 : return CODE_ADDRESS_TO_NOT_VERIFIED; - 114 : : } - 115 : 6 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 116 : : } - 117 : : - 118 : 9 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - 119 : : internal - 120 : : view - 121 : : override - 122 : : returns (uint8) - 123 : : { - 124 [ + ]: 9 : if (address(identityRegistry) == address(0)) { - 125 : 1 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 126 : : } - 127 [ + ]: 8 : if (to == address(0)) { - 128 : 1 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 129 : : } - 130 [ + ]: 7 : if (spender != address(0) && !identityRegistry.isVerified(spender)) { - 131 : 4 : return CODE_ADDRESS_SPENDER_NOT_VERIFIED; - 132 : : } - 133 : 3 : return _detectTransferRestriction(from, to, value); - 134 : : } - 135 : : - 136 : 2 : function _transferred(address from, address to, uint256 value) internal view virtual override { - 137 : 2 : uint8 code = _detectTransferRestriction(from, to, value); - 138 [ + + ]: 2 : require( - 139 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), - 140 : : RuleIdentityRegistry_InvalidTransfer(address(this), from, to, value, code) - 141 : : ); - 142 : : } - 143 : : - 144 : 2 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { - 145 : 2 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); - 146 [ + + ]: 2 : require( - 147 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), - 148 : : RuleIdentityRegistry_InvalidTransferFrom(address(this), spender, from, to, value, code) - 149 : : ); - 150 : : } - 151 : : - 152 : : } + 44 : : /** + 45 : : * @notice When true, ALSO require the spender to be identity-verified on `transferFrom`. + 46 : : * @dev Defaults to FALSE: ERC-3643 does not require it ("`transferFrom` works the same way"). + 47 : : * Mint and burn are exempt from this check regardless — the minter/burner acts on its own + 48 : : * authority, not as a delegated ERC-20 spender. + 49 : : */ + 50 : : bool public checkSpender; + 51 : : + 52 : : /*////////////////////////////////////////////////////////////// + 53 : : CONSTRUCTOR + 54 : : //////////////////////////////////////////////////////////////*/ + 55 : : + 56 : : /** + 57 : : * @notice Initializes the rule with an optional identity registry. + 58 : : * @dev Pass `false, false` for the ERC-3643-conformant default (only the receiver is verified). + 59 : : * @param identityRegistry_ Identity registry address; when the zero address, the registry is left unset (checks disabled). + 60 : : * @param checkSender_ When true, also verify the sender (STRICTER than ERC-3643). + 61 : : * @param checkSpender_ When true, also verify the spender on `transferFrom` (STRICTER than ERC-3643). + 62 : : */ + 63 : 39 : constructor(address identityRegistry_, bool checkSender_, bool checkSpender_) { + 64 [ + ]: 39 : if (identityRegistry_ != address(0)) { + 65 : 37 : identityRegistry = IIdentityRegistryVerified(identityRegistry_); + 66 : : } + 67 : 39 : checkSender = checkSender_; + 68 : 39 : checkSpender = checkSpender_; + 69 : 39 : emit IdentityCheckSenderUpdated(checkSender_); + 70 : 39 : emit IdentityCheckSpenderUpdated(checkSpender_); + 71 : : } + 72 : : + 73 : : /*////////////////////////////////////////////////////////////// + 74 : : ACCESS CONTROL + 75 : : //////////////////////////////////////////////////////////////*/ + 76 : : + 77 : 5 : modifier onlyIdentityRegistryManager() { + 78 : 5 : _authorizeIdentityRegistryManager(); + 79 : : _; + 80 : : } + 81 : : + 82 : : /*////////////////////////////////////////////////////////////// + 83 : : EXTERNAL FUNCTIONS + 84 : : //////////////////////////////////////////////////////////////*/ + 85 : : + 86 : : /** + 87 : : * @notice Returns whether this rule can produce the given restriction code. + 88 : : * @param restrictionCode Restriction code to test. + 89 : : * @return True if `restrictionCode` is one of this rule's identity-verification codes. + 90 : : */ + 91 : 4 : function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { + 92 : 4 : return restrictionCode == CODE_ADDRESS_FROM_NOT_VERIFIED || restrictionCode == CODE_ADDRESS_TO_NOT_VERIFIED + 93 : 2 : || restrictionCode == CODE_ADDRESS_SPENDER_NOT_VERIFIED; + 94 : : } + 95 : : + 96 : : /*////////////////////////////////////////////////////////////// + 97 : : PUBLIC FUNCTIONS + 98 : : //////////////////////////////////////////////////////////////*/ + 99 : : + 100 : : /** + 101 : : * @notice Sets the identity registry consulted during transfer checks. + 102 : : * @param newRegistry New identity registry address; must not be the zero address. + 103 : : */ + 104 : 4 : function setIdentityRegistry(address newRegistry) public onlyIdentityRegistryManager { + 105 [ + + ]: 2 : require(newRegistry != address(0), RuleIdentityRegistry_RegistryAddressZeroNotAllowed()); + 106 : 1 : identityRegistry = IIdentityRegistryVerified(newRegistry); + 107 : 1 : emit IdentityRegistryUpdated(newRegistry); + 108 : : } + 109 : : + 110 : : /** + 111 : : * @notice Enables or disables the (non-ERC-3643) sender verification check. + 112 : : * @dev STRICTER than ERC-3643, which verifies only the receiver. Enabling this prevents a + 113 : : * de-listed holder from exiting their position. + 114 : : * @param value The new value of the `checkSender` flag. + 115 : : */ + 116 : 2 : function setCheckSender(bool value) public virtual onlyIdentityRegistryManager { + 117 : 2 : checkSender = value; + 118 : 2 : emit IdentityCheckSenderUpdated(value); + 119 : : } + 120 : : + 121 : : /** + 122 : : * @notice Enables or disables the (non-ERC-3643) spender verification check on `transferFrom`. + 123 : : * @dev STRICTER than ERC-3643. Mint and burn remain exempt regardless. + 124 : : * @param value The new value of the `checkSpender` flag. + 125 : : */ + 126 : 5 : function setCheckSpender(bool value) public virtual onlyIdentityRegistryManager { + 127 : 5 : checkSpender = value; + 128 : 5 : emit IdentityCheckSpenderUpdated(value); + 129 : : } + 130 : : + 131 : : /** + 132 : : * @notice Clears the identity registry, disabling identity checks (all transfers pass this rule). + 133 : : */ + 134 : 5 : function clearIdentityRegistry() public onlyIdentityRegistryManager { + 135 : 3 : identityRegistry = IIdentityRegistryVerified(address(0)); + 136 : 3 : emit IdentityRegistryUpdated(address(0)); + 137 : : } + 138 : : + 139 : : /** + 140 : : * @inheritdoc IERC3643IComplianceContract + 141 : : */ + 142 : 5 : function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { + 143 : 5 : _transferred(from, to, value); + 144 : : } + 145 : : + 146 : : /** + 147 : : * @inheritdoc IRuleEngine + 148 : : */ + 149 : 7 : function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { + 150 : 7 : _transferredFrom(spender, from, to, value); + 151 : : } + 152 : : + 153 : : /** + 154 : : * @inheritdoc IERC1404 + 155 : : */ + 156 : 4 : function messageForTransferRestriction(uint8 restrictionCode) + 157 : : public + 158 : : pure + 159 : : override(IERC1404) + 160 : : returns (string memory) + 161 : : { + 162 [ + + ]: 4 : if (restrictionCode == CODE_ADDRESS_FROM_NOT_VERIFIED) { + 163 : 1 : return TEXT_ADDRESS_FROM_NOT_VERIFIED; + 164 [ + + ]: 3 : } else if (restrictionCode == CODE_ADDRESS_TO_NOT_VERIFIED) { + 165 : 1 : return TEXT_ADDRESS_TO_NOT_VERIFIED; + 166 [ + ]: 2 : } else if (restrictionCode == CODE_ADDRESS_SPENDER_NOT_VERIFIED) { + 167 : 1 : return TEXT_ADDRESS_SPENDER_NOT_VERIFIED; + 168 : : } + 169 : 1 : return TEXT_CODE_NOT_FOUND; + 170 : : } + 171 : : + 172 : : /*////////////////////////////////////////////////////////////// + 173 : : INTERNAL FUNCTIONS + 174 : : //////////////////////////////////////////////////////////////*/ + 175 : : + 176 : : /** + 177 : : * @notice Authorization hook invoked before updating or clearing the identity registry. + 178 : : */ + 179 : 0 : function _authorizeIdentityRegistryManager() internal view virtual; + 180 : : + 181 : : /** + 182 : : * @notice Detects the restriction code for a direct transfer, verifying `from` and `to` against the registry. + 183 : : * @param from Sender address; must be verified unless it is the zero address (mint). + 184 : : * @param to Recipient address; must be verified unless it is the zero address (burn). + 185 : : * @return The applicable restriction code, or TRANSFER_OK when no restriction applies. + 186 : : */ + 187 : 62 : function _detectTransferRestriction( + 188 : : address from, + 189 : : address to, + 190 : : uint256 /* value */ + 191 : : ) + 192 : : internal + 193 : : view + 194 : : override + 195 : : returns (uint8) + 196 : : { + 197 [ + ]: 62 : if (address(identityRegistry) == address(0)) { + 198 : 3 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 199 : : } + 200 : : // ERC-3643: "The `burn` function bypasses all checks on eligibility." + 201 [ + ]: 59 : if (to == address(0)) { + 202 : 3 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 203 : : } + 204 : : + 205 : : // OPT-IN, stricter than ERC-3643. Mints carry no sender, so they are exempt. + 206 [ + ]: 56 : if (checkSender && from != address(0) && !identityRegistry.isVerified(from)) { + 207 : 1 : return CODE_ADDRESS_FROM_NOT_VERIFIED; + 208 : : } + 209 : : + 210 : : // MANDATED by ERC-3643: the receiver must be verified. This is the only required check, + 211 : : // and it applies identically to `transfer`, `transferFrom` and `mint`. + 212 [ + ]: 55 : if (!identityRegistry.isVerified(to)) { + 213 : 6 : return CODE_ADDRESS_TO_NOT_VERIFIED; + 214 : : } + 215 : 49 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 216 : : } + 217 : : + 218 : : /** + 219 : : * @notice Detects the restriction code for a `transferFrom`, verifying `spender` and delegating to the direct check. + 220 : : * @param spender Approved spender initiating the transfer; must be verified unless it is the zero address. + 221 : : * @param from Sender address, forwarded to the direct transfer check. + 222 : : * @param to Recipient address, forwarded to the direct transfer check. + 223 : : * @param value Transfer amount, forwarded to the direct transfer check. + 224 : : * @return The applicable restriction code, or TRANSFER_OK when no restriction applies. + 225 : : */ + 226 : 31 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + 227 : : internal + 228 : : view + 229 : : override + 230 : : returns (uint8) + 231 : : { + 232 [ + ]: 31 : if (address(identityRegistry) == address(0)) { + 233 : 1 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 234 : : } + 235 : : // ERC-3643: burn bypasses all eligibility checks. + 236 [ + ]: 30 : if (to == address(0)) { + 237 : 2 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 238 : : } + 239 : : + 240 : : // OPT-IN, stricter than ERC-3643 ("`transferFrom` works the same way" — receiver only). + 241 : : // Mint (from == 0) and burn (to == 0) are exempt: the minter/burner acts on its own + 242 : : // authority, not as a delegated ERC-20 spender. This is what makes an unverified MINTER + 243 : : // able to mint to a verified recipient, exactly as the specification requires. + 244 : : if ( + 245 : 5 : checkSpender && spender != address(0) && from != address(0) && to != address(0) + 246 : 4 : && !identityRegistry.isVerified(spender) + 247 [ + ]: 3 : ) { + 248 : 3 : return CODE_ADDRESS_SPENDER_NOT_VERIFIED; + 249 : : } + 250 : 25 : return _detectTransferRestriction(from, to, value); + 251 : : } + 252 : : + 253 : : /** + 254 : : * @inheritdoc RuleNFTAdapter + 255 : : */ + 256 : 11 : function _transferred(address from, address to, uint256 value) internal view virtual override { + 257 : 11 : uint8 code = _detectTransferRestriction(from, to, value); + 258 [ + + ]: 11 : require( + 259 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), + 260 : : RuleIdentityRegistry_InvalidTransfer(address(this), from, to, value, code) + 261 : : ); + 262 : : } + 263 : : + 264 : : /** + 265 : : * @inheritdoc RuleNFTAdapter + 266 : : */ + 267 : 13 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { + 268 : 13 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); + 269 [ + + ]: 13 : require( + 270 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), + 271 : : RuleIdentityRegistry_InvalidTransferFrom(address(this), spender, from, to, value, code) + 272 : : ); + 273 : : } + 274 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol.func-sort-c.html index 1c84b39..432a05a 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 12 @@ -69,56 +69,56 @@ Hit count Sort by hit count - RuleMaxTotalSupplyBase._authorizeMaxTotalSupplyManager + RuleMaxTotalSupplyBase._authorizeMaxTotalSupplyManager 0 - RuleMaxTotalSupplyBase._detectTransferRestrictionFrom + RuleMaxTotalSupplyBase._transferred 2 - RuleMaxTotalSupplyBase._transferred + RuleMaxTotalSupplyBase._transferredFrom 2 - RuleMaxTotalSupplyBase._transferredFrom + RuleMaxTotalSupplyBase.canReturnTransferRestrictionCode 2 - RuleMaxTotalSupplyBase.canReturnTransferRestrictionCode + RuleMaxTotalSupplyBase.messageForTransferRestriction 2 - RuleMaxTotalSupplyBase.messageForTransferRestriction + RuleMaxTotalSupplyBase.transferred.0 2 - RuleMaxTotalSupplyBase.transferred.0 + RuleMaxTotalSupplyBase.transferred.1 2 - RuleMaxTotalSupplyBase.transferred.1 - 2 + RuleMaxTotalSupplyBase._detectTransferRestrictionFrom + 3 - RuleMaxTotalSupplyBase.setTokenContract + RuleMaxTotalSupplyBase.setTokenContract 4 - RuleMaxTotalSupplyBase.constructor - 25 + RuleMaxTotalSupplyBase.onlyMaxTotalSupplyManager + 260 - RuleMaxTotalSupplyBase.onlyMaxTotalSupplyManager + RuleMaxTotalSupplyBase.setMaxTotalSupply 260 - RuleMaxTotalSupplyBase.setMaxTotalSupply - 260 + RuleMaxTotalSupplyBase.constructor + 542 - RuleMaxTotalSupplyBase._detectTransferRestriction - 271 + RuleMaxTotalSupplyBase._detectTransferRestriction + 787
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol.func.html index 6cd4772..2a835c4 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 12 @@ -69,55 +69,55 @@ Hit count Sort by hit count - RuleMaxTotalSupplyBase._authorizeMaxTotalSupplyManager + RuleMaxTotalSupplyBase._authorizeMaxTotalSupplyManager 0 - RuleMaxTotalSupplyBase._detectTransferRestriction - 271 + RuleMaxTotalSupplyBase._detectTransferRestriction + 787 - RuleMaxTotalSupplyBase._detectTransferRestrictionFrom - 2 + RuleMaxTotalSupplyBase._detectTransferRestrictionFrom + 3 - RuleMaxTotalSupplyBase._transferred + RuleMaxTotalSupplyBase._transferred 2 - RuleMaxTotalSupplyBase._transferredFrom + RuleMaxTotalSupplyBase._transferredFrom 2 - RuleMaxTotalSupplyBase.canReturnTransferRestrictionCode + RuleMaxTotalSupplyBase.canReturnTransferRestrictionCode 2 - RuleMaxTotalSupplyBase.constructor - 25 + RuleMaxTotalSupplyBase.constructor + 542 - RuleMaxTotalSupplyBase.messageForTransferRestriction + RuleMaxTotalSupplyBase.messageForTransferRestriction 2 - RuleMaxTotalSupplyBase.onlyMaxTotalSupplyManager + RuleMaxTotalSupplyBase.onlyMaxTotalSupplyManager 260 - RuleMaxTotalSupplyBase.setMaxTotalSupply + RuleMaxTotalSupplyBase.setMaxTotalSupply 260 - RuleMaxTotalSupplyBase.setTokenContract + RuleMaxTotalSupplyBase.setTokenContract 4 - RuleMaxTotalSupplyBase.transferred.0 + RuleMaxTotalSupplyBase.transferred.0 2 - RuleMaxTotalSupplyBase.transferred.1 + RuleMaxTotalSupplyBase.transferred.1 2 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol.gcov.html index d985499..8a1df7b 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 12 @@ -84,123 +84,179 @@ 13 : : * @notice Restricts minting so that total supply never exceeds a maximum value. 14 : : */ 15 : : abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotalSupplyInvariantStorage { - 16 : : /// @dev tokenContract is trusted to return a correct totalSupply. - 17 : : ITotalSupply public tokenContract; - 18 : : uint256 public maxTotalSupply; - 19 : : - 20 : : /*////////////////////////////////////////////////////////////// - 21 : : CONSTRUCTOR - 22 : : //////////////////////////////////////////////////////////////*/ - 23 : : - 24 : 25 : constructor(address tokenContract_, uint256 maxTotalSupply_) { - 25 [ + + ]: 25 : require(tokenContract_ != address(0), RuleMaxTotalSupply_TokenAddressZeroNotAllowed()); - 26 : 24 : tokenContract = ITotalSupply(tokenContract_); - 27 : 24 : maxTotalSupply = maxTotalSupply_; - 28 : : } - 29 : : - 30 : : /*////////////////////////////////////////////////////////////// - 31 : : EXTERNAL FUNCTIONS - 32 : : //////////////////////////////////////////////////////////////*/ - 33 : : - 34 : 2 : function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { - 35 : 2 : return restrictionCode == CODE_MAX_TOTAL_SUPPLY_EXCEEDED; - 36 : : } - 37 : : - 38 : : /*////////////////////////////////////////////////////////////// - 39 : : PUBLIC FUNCTIONS - 40 : : //////////////////////////////////////////////////////////////*/ - 41 : : - 42 : 260 : function setMaxTotalSupply(uint256 newMaxTotalSupply) public onlyMaxTotalSupplyManager { - 43 : 258 : maxTotalSupply = newMaxTotalSupply; - 44 : 258 : emit MaxTotalSupplyUpdated(newMaxTotalSupply); - 45 : : } - 46 : : - 47 : 4 : function setTokenContract(address newTokenContract) public onlyMaxTotalSupplyManager { - 48 [ + + ]: 2 : require(newTokenContract != address(0), RuleMaxTotalSupply_TokenAddressZeroNotAllowed()); - 49 : 1 : tokenContract = ITotalSupply(newTokenContract); - 50 : 1 : emit TokenContractUpdated(newTokenContract); + 16 : : /** + 17 : : * @dev tokenContract is trusted to return a correct totalSupply. + 18 : : */ + 19 : : ITotalSupply public tokenContract; + 20 : : /** + 21 : : * @notice Maximum total supply; minting that would exceed this value is rejected. + 22 : : */ + 23 : : uint256 public maxTotalSupply; + 24 : : + 25 : : /*////////////////////////////////////////////////////////////// + 26 : : CONSTRUCTOR + 27 : : //////////////////////////////////////////////////////////////*/ + 28 : : + 29 : : /** + 30 : : * @notice Initializes the rule with the token to observe and the supply cap. + 31 : : * @param tokenContract_ Address of the token whose `totalSupply` is checked; must not be the zero address. + 32 : : * @param maxTotalSupply_ Maximum total supply allowed. + 33 : : */ + 34 : 542 : constructor(address tokenContract_, uint256 maxTotalSupply_) { + 35 [ + + ]: 542 : require(tokenContract_ != address(0), RuleMaxTotalSupply_TokenAddressZeroNotAllowed()); + 36 : 541 : tokenContract = ITotalSupply(tokenContract_); + 37 : 541 : maxTotalSupply = maxTotalSupply_; + 38 : : } + 39 : : + 40 : : /*////////////////////////////////////////////////////////////// + 41 : : EXTERNAL FUNCTIONS + 42 : : //////////////////////////////////////////////////////////////*/ + 43 : : + 44 : : /** + 45 : : * @notice Returns whether this rule can produce the given restriction code. + 46 : : * @param restrictionCode Restriction code to test. + 47 : : * @return True if `restrictionCode` is the max-total-supply-exceeded code. + 48 : : */ + 49 : 2 : function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { + 50 : 2 : return restrictionCode == CODE_MAX_TOTAL_SUPPLY_EXCEEDED; 51 : : } 52 : : - 53 : 2 : function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { - 54 : 2 : _transferred(from, to, value); - 55 : : } + 53 : : /*////////////////////////////////////////////////////////////// + 54 : : PUBLIC FUNCTIONS + 55 : : //////////////////////////////////////////////////////////////*/ 56 : : - 57 : 2 : function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { - 58 : 2 : _transferredFrom(spender, from, to, value); - 59 : : } - 60 : : - 61 : 2 : function messageForTransferRestriction(uint8 restrictionCode) - 62 : : public - 63 : : pure - 64 : : override(IERC1404) - 65 : : returns (string memory) - 66 : : { - 67 [ + ]: 2 : if (restrictionCode == CODE_MAX_TOTAL_SUPPLY_EXCEEDED) { - 68 : 1 : return TEXT_MAX_TOTAL_SUPPLY_EXCEEDED; - 69 : : } - 70 : 1 : return TEXT_CODE_NOT_FOUND; - 71 : : } - 72 : : - 73 : : /*////////////////////////////////////////////////////////////// - 74 : : ACCESS CONTROL - 75 : : //////////////////////////////////////////////////////////////*/ - 76 : : - 77 : 260 : modifier onlyMaxTotalSupplyManager() { - 78 : 260 : _authorizeMaxTotalSupplyManager(); - 79 : : _; - 80 : : } - 81 : : - 82 : 0 : function _authorizeMaxTotalSupplyManager() internal view virtual; - 83 : : - 84 : : /*////////////////////////////////////////////////////////////// - 85 : : INTERNAL FUNCTIONS - 86 : : //////////////////////////////////////////////////////////////*/ - 87 : : - 88 : 271 : function _detectTransferRestriction( - 89 : : address from, - 90 : : address, - 91 : : /* to */ - 92 : : uint256 value - 93 : : ) - 94 : : internal - 95 : : view - 96 : : override - 97 : : returns (uint8) + 57 : : /** + 58 : : * @notice Updates the maximum total supply. + 59 : : * @param newMaxTotalSupply New maximum total supply value. + 60 : : */ + 61 : 260 : function setMaxTotalSupply(uint256 newMaxTotalSupply) public onlyMaxTotalSupplyManager { + 62 : 258 : maxTotalSupply = newMaxTotalSupply; + 63 : 258 : emit MaxTotalSupplyUpdated(newMaxTotalSupply); + 64 : : } + 65 : : + 66 : : /** + 67 : : * @notice Updates the token contract whose total supply is checked. + 68 : : * @param newTokenContract New token contract address; must not be the zero address. + 69 : : */ + 70 : 4 : function setTokenContract(address newTokenContract) public onlyMaxTotalSupplyManager { + 71 [ + + ]: 2 : require(newTokenContract != address(0), RuleMaxTotalSupply_TokenAddressZeroNotAllowed()); + 72 : 1 : tokenContract = ITotalSupply(newTokenContract); + 73 : 1 : emit TokenContractUpdated(newTokenContract); + 74 : : } + 75 : : + 76 : : /** + 77 : : * @inheritdoc IERC3643IComplianceContract + 78 : : */ + 79 : 2 : function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { + 80 : 2 : _transferred(from, to, value); + 81 : : } + 82 : : + 83 : : /** + 84 : : * @inheritdoc IRuleEngine + 85 : : */ + 86 : 2 : function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { + 87 : 2 : _transferredFrom(spender, from, to, value); + 88 : : } + 89 : : + 90 : : /** + 91 : : * @inheritdoc IERC1404 + 92 : : */ + 93 : 2 : function messageForTransferRestriction(uint8 restrictionCode) + 94 : : public + 95 : : pure + 96 : : override(IERC1404) + 97 : : returns (string memory) 98 : : { - 99 [ + ]: 271 : if (from == address(0)) { - 100 : 268 : uint256 currentSupply = tokenContract.totalSupply(); - 101 [ + ]: 268 : if (currentSupply + value > maxTotalSupply) { - 102 : 186 : return CODE_MAX_TOTAL_SUPPLY_EXCEEDED; - 103 : : } - 104 : : } - 105 : 85 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 106 : : } - 107 : : - 108 : 2 : function _detectTransferRestrictionFrom(address, address from, address to, uint256 value) - 109 : : internal - 110 : : view - 111 : : override - 112 : : returns (uint8) - 113 : : { - 114 : 2 : return _detectTransferRestriction(from, to, value); - 115 : : } - 116 : : - 117 : 2 : function _transferred(address from, address to, uint256 value) internal view virtual { - 118 : 2 : uint8 code = _detectTransferRestriction(from, to, value); - 119 [ + + ]: 2 : require( - 120 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), - 121 : : RuleMaxTotalSupply_InvalidTransfer(address(this), from, to, value, code) - 122 : : ); - 123 : : } - 124 : : - 125 : 2 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual { - 126 : 2 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); - 127 [ + + ]: 2 : require( - 128 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), - 129 : : RuleMaxTotalSupply_InvalidTransferFrom(address(this), spender, from, to, value, code) - 130 : : ); - 131 : : } - 132 : : } + 99 [ + ]: 2 : if (restrictionCode == CODE_MAX_TOTAL_SUPPLY_EXCEEDED) { + 100 : 1 : return TEXT_MAX_TOTAL_SUPPLY_EXCEEDED; + 101 : : } + 102 : 1 : return TEXT_CODE_NOT_FOUND; + 103 : : } + 104 : : + 105 : : /*////////////////////////////////////////////////////////////// + 106 : : ACCESS CONTROL + 107 : : //////////////////////////////////////////////////////////////*/ + 108 : : + 109 : 260 : modifier onlyMaxTotalSupplyManager() { + 110 : 260 : _authorizeMaxTotalSupplyManager(); + 111 : : _; + 112 : : } + 113 : : + 114 : : /** + 115 : : * @notice Authorization hook invoked before updating the max total supply or token contract. + 116 : : */ + 117 : 0 : function _authorizeMaxTotalSupplyManager() internal view virtual; + 118 : : + 119 : : /*////////////////////////////////////////////////////////////// + 120 : : INTERNAL FUNCTIONS + 121 : : //////////////////////////////////////////////////////////////*/ + 122 : : + 123 : : /** + 124 : : * @inheritdoc RuleTransferValidation + 125 : : */ + 126 : 787 : function _detectTransferRestriction( + 127 : : address from, + 128 : : address, + 129 : : /* to */ + 130 : : uint256 value + 131 : : ) + 132 : : internal + 133 : : view + 134 : : override + 135 : : returns (uint8) + 136 : : { + 137 [ + ]: 787 : if (from == address(0)) { + 138 : 784 : uint256 currentSupply = tokenContract.totalSupply(); + 139 : : // Overflow-safe: `currentSupply + value` could exceed uint256 and this is a + 140 : : // MUST-NOT-revert ERC-1404/ERC-3643 view, so compare against the remaining headroom. + 141 [ + ]: 784 : if (currentSupply > maxTotalSupply || value > maxTotalSupply - currentSupply) { + 142 : 458 : return CODE_MAX_TOTAL_SUPPLY_EXCEEDED; + 143 : : } + 144 : : } + 145 : 329 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 146 : : } + 147 : : + 148 : : /** + 149 : : * @inheritdoc RuleTransferValidation + 150 : : */ + 151 : 3 : function _detectTransferRestrictionFrom(address, address from, address to, uint256 value) + 152 : : internal + 153 : : view + 154 : : override + 155 : : returns (uint8) + 156 : : { + 157 : 3 : return _detectTransferRestriction(from, to, value); + 158 : : } + 159 : : + 160 : : /** + 161 : : * @notice Enforces the max-total-supply restriction for a direct transfer, reverting on violation. + 162 : : * @param from Sender address; the zero address denotes a mint whose supply is checked. + 163 : : * @param to Recipient address. + 164 : : * @param value Transfer amount. + 165 : : */ + 166 : 2 : function _transferred(address from, address to, uint256 value) internal view virtual { + 167 : 2 : uint8 code = _detectTransferRestriction(from, to, value); + 168 [ + + ]: 2 : require( + 169 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), + 170 : : RuleMaxTotalSupply_InvalidTransfer(address(this), from, to, value, code) + 171 : : ); + 172 : : } + 173 : : + 174 : : /** + 175 : : * @notice Enforces the max-total-supply restriction for a `transferFrom`, reverting on violation. + 176 : : * @param spender Approved spender initiating the transfer. + 177 : : * @param from Sender address; the zero address denotes a mint whose supply is checked. + 178 : : * @param to Recipient address. + 179 : : * @param value Transfer amount. + 180 : : */ + 181 : 2 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual { + 182 : 2 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); + 183 [ + + ]: 2 : require( + 184 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), + 185 : : RuleMaxTotalSupply_InvalidTransferFrom(address(this), spender, from, to, value, code) + 186 : : ); + 187 : : } + 188 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSanctionsListBase.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSanctionsListBase.sol.func-sort-c.html index fdef284..91a2f97 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSanctionsListBase.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSanctionsListBase.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 13 @@ -69,60 +69,60 @@ Hit count Sort by hit count - RuleSanctionsListBase._authorizeSanctionListManager + RuleSanctionsListBase._authorizeSanctionListManager 0 - RuleSanctionsListBase.transferred.1 - 2 + RuleSanctionsListBase.canReturnTransferRestrictionCode + 3 - RuleSanctionsListBase._transferredFrom + RuleSanctionsListBase.clearSanctionListOracle 3 - RuleSanctionsListBase.canReturnTransferRestrictionCode + RuleSanctionsListBase.onlySanctionListManager 3 - RuleSanctionsListBase.clearSanctionListOracle - 3 + RuleSanctionsListBase.messageForTransferRestriction + 4 - RuleSanctionsListBase.onlySanctionListManager - 3 + RuleSanctionsListBase.transferred.0 + 9 - RuleSanctionsListBase.messageForTransferRestriction - 4 + RuleSanctionsListBase.setSanctionListOracle + 18 - RuleSanctionsListBase._detectTransferRestrictionFrom - 16 + RuleSanctionsListBase._transferred + 19 - RuleSanctionsListBase.setSanctionListOracle - 17 + RuleSanctionsListBase._setSanctionListOracle + 39 - RuleSanctionsListBase._setSanctionListOracle - 37 + RuleSanctionsListBase.transferred.1 + 41 - RuleSanctionsListBase.transferred.0 - 43 + RuleSanctionsListBase._transferredFrom + 48 - RuleSanctionsListBase.constructor - 46 + RuleSanctionsListBase.constructor + 49 - RuleSanctionsListBase._transferred - 47 + RuleSanctionsListBase._detectTransferRestrictionFrom + 69 - RuleSanctionsListBase._detectTransferRestriction - 84 + RuleSanctionsListBase._detectTransferRestriction + 119
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSanctionsListBase.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSanctionsListBase.sol.func.html index e7987cd..6e74be9 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSanctionsListBase.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSanctionsListBase.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 13 @@ -69,60 +69,60 @@ Hit count Sort by hit count - RuleSanctionsListBase._authorizeSanctionListManager + RuleSanctionsListBase._authorizeSanctionListManager 0 - RuleSanctionsListBase._detectTransferRestriction - 84 + RuleSanctionsListBase._detectTransferRestriction + 119 - RuleSanctionsListBase._detectTransferRestrictionFrom - 16 + RuleSanctionsListBase._detectTransferRestrictionFrom + 69 - RuleSanctionsListBase._setSanctionListOracle - 37 + RuleSanctionsListBase._setSanctionListOracle + 39 - RuleSanctionsListBase._transferred - 47 + RuleSanctionsListBase._transferred + 19 - RuleSanctionsListBase._transferredFrom - 3 + RuleSanctionsListBase._transferredFrom + 48 - RuleSanctionsListBase.canReturnTransferRestrictionCode + RuleSanctionsListBase.canReturnTransferRestrictionCode 3 - RuleSanctionsListBase.clearSanctionListOracle + RuleSanctionsListBase.clearSanctionListOracle 3 - RuleSanctionsListBase.constructor - 46 + RuleSanctionsListBase.constructor + 49 - RuleSanctionsListBase.messageForTransferRestriction + RuleSanctionsListBase.messageForTransferRestriction 4 - RuleSanctionsListBase.onlySanctionListManager + RuleSanctionsListBase.onlySanctionListManager 3 - RuleSanctionsListBase.setSanctionListOracle - 17 + RuleSanctionsListBase.setSanctionListOracle + 18 - RuleSanctionsListBase.transferred.0 - 43 + RuleSanctionsListBase.transferred.0 + 9 - RuleSanctionsListBase.transferred.1 - 2 + RuleSanctionsListBase.transferred.1 + 41
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSanctionsListBase.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSanctionsListBase.sol.gcov.html index 4696c16..ad6b1b2 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSanctionsListBase.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSanctionsListBase.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 13 @@ -86,138 +86,202 @@ 15 : : * @notice Compliance rule enforcing sanctions-screening for token transfers. 16 : : */ 17 : : abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapter, RuleSanctionsListInvariantStorage { - 18 : : ISanctionsList public sanctionsList; - 19 : : - 20 : : /*////////////////////////////////////////////////////////////// - 21 : : CONSTRUCTOR - 22 : : //////////////////////////////////////////////////////////////*/ - 23 : : - 24 : 46 : constructor(address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) - 25 : : MetaTxModuleStandalone(forwarderIrrevocable) - 26 : : { - 27 [ + ]: 45 : if (address(sanctionContractOracle_) != address(0)) { - 28 : 20 : _setSanctionListOracle(sanctionContractOracle_); - 29 : : } - 30 : : } - 31 : : - 32 : : /*////////////////////////////////////////////////////////////// - 33 : : EXTERNAL FUNCTIONS - 34 : : //////////////////////////////////////////////////////////////*/ - 35 : : - 36 : 3 : function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { - 37 : 3 : return restrictionCode == CODE_ADDRESS_FROM_IS_SANCTIONED || restrictionCode == CODE_ADDRESS_TO_IS_SANCTIONED - 38 : 1 : || restrictionCode == CODE_ADDRESS_SPENDER_IS_SANCTIONED; - 39 : : } - 40 : : - 41 : : /*////////////////////////////////////////////////////////////// - 42 : : PUBLIC FUNCTIONS - 43 : : //////////////////////////////////////////////////////////////*/ - 44 : : - 45 : 17 : function setSanctionListOracle(ISanctionsList sanctionContractOracle_) public virtual onlySanctionListManager { - 46 [ + + ]: 15 : require(address(sanctionContractOracle_) != address(0), RuleSanctionsList_OracleAddressZeroNotAllowed()); - 47 : 14 : _setSanctionListOracle(sanctionContractOracle_); - 48 : : } - 49 : : - 50 : 3 : function clearSanctionListOracle() public virtual onlySanctionListManager { - 51 : 3 : _setSanctionListOracle(ISanctionsList(address(0))); - 52 : : } - 53 : : - 54 : 43 : function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { - 55 : 43 : _transferred(from, to, value); - 56 : : } - 57 : : - 58 : 2 : function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { - 59 : 2 : _transferredFrom(spender, from, to, value); - 60 : : } - 61 : : - 62 : 4 : function messageForTransferRestriction(uint8 restrictionCode) - 63 : : public - 64 : : pure - 65 : : override(IERC1404) - 66 : : returns (string memory) - 67 : : { - 68 [ + + ]: 4 : if (restrictionCode == CODE_ADDRESS_FROM_IS_SANCTIONED) { - 69 : 1 : return TEXT_ADDRESS_FROM_IS_SANCTIONED; - 70 [ + + ]: 3 : } else if (restrictionCode == CODE_ADDRESS_TO_IS_SANCTIONED) { - 71 : 1 : return TEXT_ADDRESS_TO_IS_SANCTIONED; - 72 [ + ]: 2 : } else if (restrictionCode == CODE_ADDRESS_SPENDER_IS_SANCTIONED) { - 73 : 1 : return TEXT_ADDRESS_SPENDER_IS_SANCTIONED; - 74 : : } - 75 : 1 : return TEXT_CODE_NOT_FOUND; - 76 : : } - 77 : : - 78 : : /*////////////////////////////////////////////////////////////// - 79 : : ACCESS CONTROL - 80 : : //////////////////////////////////////////////////////////////*/ - 81 : : - 82 : 3 : modifier onlySanctionListManager() { - 83 : 3 : _authorizeSanctionListManager(); - 84 : : _; - 85 : : } - 86 : : - 87 : 0 : function _authorizeSanctionListManager() internal view virtual; - 88 : : - 89 : : /*////////////////////////////////////////////////////////////// - 90 : : INTERNAL FUNCTIONS - 91 : : //////////////////////////////////////////////////////////////*/ - 92 : : - 93 : 84 : function _detectTransferRestriction( - 94 : : address from, - 95 : : address to, - 96 : : uint256 /* value */ - 97 : : ) - 98 : : internal - 99 : : view - 100 : : override - 101 : : returns (uint8) - 102 : : { - 103 [ + ]: 84 : if (address(sanctionsList) != address(0)) { - 104 [ + + ]: 77 : if (sanctionsList.isSanctioned(from)) { - 105 : 10 : return CODE_ADDRESS_FROM_IS_SANCTIONED; - 106 [ + ]: 67 : } else if (sanctionsList.isSanctioned(to)) { - 107 : 12 : return CODE_ADDRESS_TO_IS_SANCTIONED; - 108 : : } - 109 : : } - 110 : 62 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); - 111 : : } - 112 : : - 113 : 16 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - 114 : : internal - 115 : : view - 116 : : virtual - 117 : : override - 118 : : returns (uint8) - 119 : : { - 120 [ + ]: 16 : if (address(sanctionsList) != address(0)) { - 121 [ + ]: 15 : if (sanctionsList.isSanctioned(spender)) { - 122 : 6 : return CODE_ADDRESS_SPENDER_IS_SANCTIONED; - 123 : : } - 124 : 9 : return _detectTransferRestriction(from, to, value); - 125 : : } - 126 : 1 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 18 : : /** + 19 : : * @notice The sanctions oracle consulted on each transfer; unset disables screening. + 20 : : */ + 21 : : ISanctionsList public sanctionsList; + 22 : : + 23 : : /*////////////////////////////////////////////////////////////// + 24 : : CONSTRUCTOR + 25 : : //////////////////////////////////////////////////////////////*/ + 26 : : + 27 : : /** + 28 : : * @notice Deploys the sanctions-list rule base and optionally sets the oracle. + 29 : : * @param forwarderIrrevocable Trusted ERC-2771 forwarder address for meta-transactions. + 30 : : * @param sanctionContractOracle_ Initial sanctions oracle; skipped when the zero address. + 31 : : */ + 32 : 49 : constructor(address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) + 33 : : MetaTxModuleStandalone(forwarderIrrevocable) + 34 : : { + 35 [ + ]: 48 : if (address(sanctionContractOracle_) != address(0)) { + 36 : 21 : _setSanctionListOracle(sanctionContractOracle_); + 37 : : } + 38 : : } + 39 : : + 40 : : /*////////////////////////////////////////////////////////////// + 41 : : EXTERNAL FUNCTIONS + 42 : : //////////////////////////////////////////////////////////////*/ + 43 : : + 44 : : /** + 45 : : * @inheritdoc IRule + 46 : : */ + 47 : 3 : function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { + 48 : 3 : return restrictionCode == CODE_ADDRESS_FROM_IS_SANCTIONED || restrictionCode == CODE_ADDRESS_TO_IS_SANCTIONED + 49 : 1 : || restrictionCode == CODE_ADDRESS_SPENDER_IS_SANCTIONED; + 50 : : } + 51 : : + 52 : : /*////////////////////////////////////////////////////////////// + 53 : : PUBLIC FUNCTIONS + 54 : : //////////////////////////////////////////////////////////////*/ + 55 : : + 56 : : /** + 57 : : * @notice Sets the sanctions oracle consulted on each transfer. + 58 : : * @dev Restricted to the sanction-list manager; reverts on the zero address. + 59 : : * @param sanctionContractOracle_ The new sanctions oracle address. + 60 : : */ + 61 : 18 : function setSanctionListOracle(ISanctionsList sanctionContractOracle_) public virtual onlySanctionListManager { + 62 [ + + ]: 16 : require(address(sanctionContractOracle_) != address(0), RuleSanctionsList_OracleAddressZeroNotAllowed()); + 63 : 15 : _setSanctionListOracle(sanctionContractOracle_); + 64 : : } + 65 : : + 66 : : /** + 67 : : * @notice Clears the sanctions oracle, disabling sanctions screening. + 68 : : * @dev Restricted to the sanction-list manager. + 69 : : */ + 70 : 3 : function clearSanctionListOracle() public virtual onlySanctionListManager { + 71 : 3 : _setSanctionListOracle(ISanctionsList(address(0))); + 72 : : } + 73 : : + 74 : : /** + 75 : : * @inheritdoc IERC3643IComplianceContract + 76 : : */ + 77 : 9 : function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { + 78 : 9 : _transferred(from, to, value); + 79 : : } + 80 : : + 81 : : /** + 82 : : * @inheritdoc IRuleEngine + 83 : : */ + 84 : 41 : function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { + 85 : 41 : _transferredFrom(spender, from, to, value); + 86 : : } + 87 : : + 88 : : /** + 89 : : * @inheritdoc IERC1404 + 90 : : */ + 91 : 4 : function messageForTransferRestriction(uint8 restrictionCode) + 92 : : public + 93 : : pure + 94 : : override(IERC1404) + 95 : : returns (string memory) + 96 : : { + 97 [ + + ]: 4 : if (restrictionCode == CODE_ADDRESS_FROM_IS_SANCTIONED) { + 98 : 1 : return TEXT_ADDRESS_FROM_IS_SANCTIONED; + 99 [ + + ]: 3 : } else if (restrictionCode == CODE_ADDRESS_TO_IS_SANCTIONED) { + 100 : 1 : return TEXT_ADDRESS_TO_IS_SANCTIONED; + 101 [ + ]: 2 : } else if (restrictionCode == CODE_ADDRESS_SPENDER_IS_SANCTIONED) { + 102 : 1 : return TEXT_ADDRESS_SPENDER_IS_SANCTIONED; + 103 : : } + 104 : 1 : return TEXT_CODE_NOT_FOUND; + 105 : : } + 106 : : + 107 : : /*////////////////////////////////////////////////////////////// + 108 : : ACCESS CONTROL + 109 : : //////////////////////////////////////////////////////////////*/ + 110 : : + 111 : 3 : modifier onlySanctionListManager() { + 112 : 3 : _authorizeSanctionListManager(); + 113 : : _; + 114 : : } + 115 : : + 116 : : /*////////////////////////////////////////////////////////////// + 117 : : INTERNAL FUNCTIONS + 118 : : //////////////////////////////////////////////////////////////*/ + 119 : : + 120 : : /** + 121 : : * @notice Updates the stored sanctions oracle and emits {SetSanctionListOracle}. + 122 : : * @param sanctionContractOracle_ The new sanctions oracle address (may be zero to disable). + 123 : : */ + 124 : 39 : function _setSanctionListOracle(ISanctionsList sanctionContractOracle_) internal virtual { + 125 : 39 : sanctionsList = sanctionContractOracle_; + 126 : 39 : emit SetSanctionListOracle(sanctionContractOracle_); 127 : : } 128 : : - 129 : 47 : function _transferred(address from, address to, uint256 value) internal view virtual override { - 130 : 47 : uint8 code = _detectTransferRestriction(from, to, value); - 131 [ + + ]: 47 : require( - 132 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), - 133 : : RuleSanctionsList_InvalidTransfer(address(this), from, to, value, code) - 134 : : ); - 135 : : } - 136 : : - 137 : 3 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { - 138 : 3 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); - 139 [ + + ]: 3 : require( - 140 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), - 141 : : RuleSanctionsList_InvalidTransferFrom(address(this), spender, from, to, value, code) - 142 : : ); - 143 : : } - 144 : : - 145 : 37 : function _setSanctionListOracle(ISanctionsList sanctionContractOracle_) internal virtual { - 146 : 37 : sanctionsList = sanctionContractOracle_; - 147 : 37 : emit SetSanctionListOracle(sanctionContractOracle_); - 148 : : } - 149 : : } + 129 : : /** + 130 : : * @notice Authorizes the caller as sanction-list manager; reverts otherwise. + 131 : : * @dev Implemented by concrete subclasses with the desired access-control policy. + 132 : : */ + 133 : 0 : function _authorizeSanctionListManager() internal view virtual; + 134 : : + 135 : : /** + 136 : : * @notice Detects whether a direct transfer is restricted by the sanctions oracle. + 137 : : * @param from The sender address. + 138 : : * @param to The recipient address. + 139 : : * @return The restriction code, or TRANSFER_OK when no party is sanctioned. + 140 : : */ + 141 : 119 : function _detectTransferRestriction( + 142 : : address from, + 143 : : address to, + 144 : : uint256 /* value */ + 145 : : ) + 146 : : internal + 147 : : view + 148 : : override + 149 : : returns (uint8) + 150 : : { + 151 [ + ]: 119 : if (address(sanctionsList) != address(0)) { + 152 [ + + ]: 112 : if (sanctionsList.isSanctioned(from)) { + 153 : 27 : return CODE_ADDRESS_FROM_IS_SANCTIONED; + 154 [ + ]: 85 : } else if (sanctionsList.isSanctioned(to)) { + 155 : 12 : return CODE_ADDRESS_TO_IS_SANCTIONED; + 156 : : } + 157 : : } + 158 : 80 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 159 : : } + 160 : : + 161 : : /** + 162 : : * @notice Detects whether a delegated transfer is restricted by the sanctions oracle. + 163 : : * @param spender The delegated spender address. + 164 : : * @param from The sender address. + 165 : : * @param to The recipient address. + 166 : : * @param value The amount transferred. + 167 : : * @return The restriction code, or TRANSFER_OK when no party is sanctioned. + 168 : : */ + 169 : 69 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + 170 : : internal + 171 : : view + 172 : : virtual + 173 : : override + 174 : : returns (uint8) + 175 : : { + 176 [ + ]: 69 : if (address(sanctionsList) != address(0)) { + 177 [ + ]: 68 : if (sanctionsList.isSanctioned(spender)) { + 178 : 6 : return CODE_ADDRESS_SPENDER_IS_SANCTIONED; + 179 : : } + 180 : 62 : return _detectTransferRestriction(from, to, value); + 181 : : } + 182 : 1 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 183 : : } + 184 : : + 185 : : /** + 186 : : * @notice Reverts if a direct transfer is blocked by the sanctions oracle. + 187 : : * @param from The sender address. + 188 : : * @param to The recipient address. + 189 : : * @param value The amount transferred. + 190 : : */ + 191 : 19 : function _transferred(address from, address to, uint256 value) internal view virtual override { + 192 : 19 : uint8 code = _detectTransferRestriction(from, to, value); + 193 [ + + ]: 19 : require( + 194 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), + 195 : : RuleSanctionsList_InvalidTransfer(address(this), from, to, value, code) + 196 : : ); + 197 : : } + 198 : : + 199 : : /** + 200 : : * @notice Reverts if a delegated transfer is blocked by the sanctions oracle. + 201 : : * @param spender The delegated spender address. + 202 : : * @param from The sender address. + 203 : : * @param to The recipient address. + 204 : : * @param value The amount transferred. + 205 : : */ + 206 : 48 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { + 207 : 48 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); + 208 [ + + ]: 48 : require( + 209 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), + 210 : : RuleSanctionsList_InvalidTransferFrom(address(this), spender, from, to, value, code) + 211 : : ); + 212 : : } + 213 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol.func-sort-c.html index 0bbdfbe..ab13111 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 19 - 19 + 22 + 22 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 8 - 8 + 9 + 9 100.0 % @@ -69,36 +69,40 @@ Hit count Sort by hit count - RuleSpenderWhitelistBase.transferred.0 - 1 + RuleSpenderWhitelistBase.canReturnTransferRestrictionCode + 2 - RuleSpenderWhitelistBase.canReturnTransferRestrictionCode + RuleSpenderWhitelistBase.messageForTransferRestriction 2 - RuleSpenderWhitelistBase.messageForTransferRestriction - 2 + RuleSpenderWhitelistBase.transferred.0 + 3 - RuleSpenderWhitelistBase.transferred.1 - 2 + RuleSpenderWhitelistBase.transferred.1 + 6 - RuleSpenderWhitelistBase._transferred - 3 + RuleSpenderWhitelistBase.supportsInterface + 8 + + + RuleSpenderWhitelistBase._transferred + 9 - RuleSpenderWhitelistBase._detectTransferRestriction - 4 + RuleSpenderWhitelistBase._detectTransferRestriction + 12 - RuleSpenderWhitelistBase._transferredFrom - 7 + RuleSpenderWhitelistBase._transferredFrom + 17 - RuleSpenderWhitelistBase._detectTransferRestrictionFrom - 13 + RuleSpenderWhitelistBase._detectTransferRestrictionFrom + 35
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol.func.html index 7bf8a35..16f8dcf 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 19 - 19 + 22 + 22 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 8 - 8 + 9 + 9 100.0 % @@ -69,36 +69,40 @@ Hit count Sort by hit count - RuleSpenderWhitelistBase._detectTransferRestriction - 4 + RuleSpenderWhitelistBase._detectTransferRestriction + 12 - RuleSpenderWhitelistBase._detectTransferRestrictionFrom - 13 + RuleSpenderWhitelistBase._detectTransferRestrictionFrom + 35 - RuleSpenderWhitelistBase._transferred - 3 + RuleSpenderWhitelistBase._transferred + 9 - RuleSpenderWhitelistBase._transferredFrom - 7 + RuleSpenderWhitelistBase._transferredFrom + 17 - RuleSpenderWhitelistBase.canReturnTransferRestrictionCode + RuleSpenderWhitelistBase.canReturnTransferRestrictionCode 2 - RuleSpenderWhitelistBase.messageForTransferRestriction + RuleSpenderWhitelistBase.messageForTransferRestriction 2 - RuleSpenderWhitelistBase.transferred.0 - 1 + RuleSpenderWhitelistBase.supportsInterface + 8 - RuleSpenderWhitelistBase.transferred.1 - 2 + RuleSpenderWhitelistBase.transferred.0 + 3 + + + RuleSpenderWhitelistBase.transferred.1 + 6
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol.gcov.html index c081701..21b9b3e 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 19 - 19 + 22 + 22 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 8 - 8 + 9 + 9 100.0 % @@ -74,89 +74,139 @@ 3 : : 4 : : import {RuleAddressSet} from "../RuleAddressSet/RuleAddressSet.sol"; 5 : : import {RuleNFTAdapter} from "../core/RuleNFTAdapter.sol"; - 6 : : import {RuleSpenderWhitelistInvariantStorage} from "../invariant/RuleSpenderWhitelistInvariantStorage.sol"; - 7 : : import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; - 8 : : import {IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; - 9 : : import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; - 10 : : - 11 : : /** - 12 : : * @title RuleSpenderWhitelistBase - 13 : : * @notice Restricts `transferFrom`-style flows to whitelisted spenders only. - 14 : : * @dev Direct transfers (`transferred(from,to,value)`) are intentionally no-op. - 15 : : */ - 16 : : abstract contract RuleSpenderWhitelistBase is RuleAddressSet, RuleNFTAdapter, RuleSpenderWhitelistInvariantStorage { - 17 : : /*////////////////////////////////////////////////////////////// - 18 : : CONSTRUCTOR - 19 : : //////////////////////////////////////////////////////////////*/ - 20 : : - 21 : : constructor(address forwarderIrrevocable) RuleAddressSet(forwarderIrrevocable) {} + 6 : : import {RuleTransferValidation} from "../core/RuleTransferValidation.sol"; + 7 : : import {RuleSpenderWhitelistInvariantStorage} from "../invariant/RuleSpenderWhitelistInvariantStorage.sol"; + 8 : : import {AddressListInterfaceId} from "../../../interfaces/library/AddressListInterfaceId.sol"; + 9 : : import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; + 10 : : import {IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; + 11 : : import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; + 12 : : + 13 : : /** + 14 : : * @title RuleSpenderWhitelistBase + 15 : : * @notice Restricts `transferFrom`-style flows to whitelisted spenders only. + 16 : : * @dev Direct transfers (`transferred(from,to,value)`) are intentionally no-op. + 17 : : */ + 18 : : abstract contract RuleSpenderWhitelistBase is RuleAddressSet, RuleNFTAdapter, RuleSpenderWhitelistInvariantStorage { + 19 : : /*////////////////////////////////////////////////////////////// + 20 : : CONSTRUCTOR + 21 : : //////////////////////////////////////////////////////////////*/ 22 : : - 23 : : /*////////////////////////////////////////////////////////////// - 24 : : EXTERNAL FUNCTIONS - 25 : : //////////////////////////////////////////////////////////////*/ - 26 : : - 27 : 2 : function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { - 28 : 2 : return restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED; - 29 : : } - 30 : : - 31 : : /*////////////////////////////////////////////////////////////// - 32 : : PUBLIC FUNCTIONS - 33 : : //////////////////////////////////////////////////////////////*/ - 34 : : - 35 : : /** - 36 : : * @dev Regular transfers are always accepted by this rule. + 23 : : /** + 24 : : * @notice Deploys the spender-whitelist rule base. + 25 : : * @param forwarderIrrevocable Trusted ERC-2771 forwarder address for meta-transactions. + 26 : : */ + 27 : : constructor(address forwarderIrrevocable) RuleAddressSet(forwarderIrrevocable) {} + 28 : : + 29 : : /*////////////////////////////////////////////////////////////// + 30 : : EXTERNAL FUNCTIONS + 31 : : //////////////////////////////////////////////////////////////*/ + 32 : : + 33 : : /** + 34 : : * @notice Returns whether this rule can emit the given restriction code. + 35 : : * @param restrictionCode The restriction code to check. + 36 : : * @return True if the code is produced by this rule. 37 : : */ - 38 : 1 : function transferred(address, address, uint256) public view override(IERC3643IComplianceContract) {} - 39 : : - 40 : 2 : function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { - 41 : 2 : _transferredFrom(spender, from, to, value); - 42 : : } - 43 : : - 44 : 2 : function messageForTransferRestriction(uint8 restrictionCode) - 45 : : public - 46 : : pure - 47 : : override(IERC1404) - 48 : : returns (string memory) - 49 : : { - 50 [ + ]: 2 : if (restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED) { - 51 : 1 : return TEXT_ADDRESS_SPENDER_NOT_WHITELISTED; - 52 : : } - 53 : 1 : return TEXT_CODE_NOT_FOUND; - 54 : : } - 55 : : - 56 : : /*////////////////////////////////////////////////////////////// - 57 : : INTERNAL FUNCTIONS - 58 : : //////////////////////////////////////////////////////////////*/ - 59 : : - 60 : 4 : function _detectTransferRestriction(address, address, uint256) internal pure virtual override returns (uint8) { - 61 : 4 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 62 : : } - 63 : : - 64 : 13 : function _detectTransferRestrictionFrom(address spender, address, address, uint256) - 65 : : internal - 66 : : view - 67 : : virtual - 68 : : override - 69 : : returns (uint8) - 70 : : { - 71 [ + ]: 13 : if (spender != address(0) && !_isAddressListed(spender)) { - 72 : 5 : return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; - 73 : : } - 74 : 8 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 75 : : } - 76 : : - 77 : 3 : function _transferred(address, address, uint256) internal view virtual override { - 78 : : // no-op: regular transfers are intentionally ignored by this rule - 79 : : } - 80 : : - 81 : 7 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { - 82 : 7 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); - 83 [ + + ]: 7 : require( - 84 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), - 85 : : RuleSpenderWhitelist_InvalidTransferFrom(address(this), spender, from, to, value, code) - 86 : : ); - 87 : : } - 88 : : } + 38 : 2 : function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { + 39 : 2 : return restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED; + 40 : : } + 41 : : + 42 : : /*////////////////////////////////////////////////////////////// + 43 : : PUBLIC FUNCTIONS + 44 : : //////////////////////////////////////////////////////////////*/ + 45 : : + 46 : : /** + 47 : : * @dev Regular transfers are always accepted by this rule. + 48 : : */ + 49 : 3 : function transferred(address, address, uint256) public view override(IERC3643IComplianceContract) {} + 50 : : + 51 : : /** + 52 : : * @inheritdoc IRuleEngine + 53 : : */ + 54 : 6 : function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { + 55 : 6 : _transferredFrom(spender, from, to, value); + 56 : : } + 57 : : + 58 : : /** + 59 : : * @inheritdoc IERC1404 + 60 : : */ + 61 : 2 : function messageForTransferRestriction(uint8 restrictionCode) + 62 : : public + 63 : : pure + 64 : : override(IERC1404) + 65 : : returns (string memory) + 66 : : { + 67 [ + ]: 2 : if (restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED) { + 68 : 1 : return TEXT_ADDRESS_SPENDER_NOT_WHITELISTED; + 69 : : } + 70 : 1 : return TEXT_CODE_NOT_FOUND; + 71 : : } + 72 : : + 73 : : /** + 74 : : * @inheritdoc RuleTransferValidation + 75 : : */ + 76 : 8 : function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { + 77 : : // Advertise IAddressList: this rule manages an address set and is callable through + 78 : : // the IAddressList interface. + 79 : 8 : return interfaceId == AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID + 80 : 6 : || RuleTransferValidation.supportsInterface(interfaceId); + 81 : : } + 82 : : + 83 : : /*////////////////////////////////////////////////////////////// + 84 : : INTERNAL FUNCTIONS + 85 : : //////////////////////////////////////////////////////////////*/ + 86 : : + 87 : : /** + 88 : : * @notice Direct transfers are always accepted by this rule. + 89 : : * @return Always TRANSFER_OK. + 90 : : */ + 91 : 12 : function _detectTransferRestriction(address, address, uint256) internal pure virtual override returns (uint8) { + 92 : 12 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 93 : : } + 94 : : + 95 : : /** + 96 : : * @notice Detects whether a delegated transfer is blocked because the spender is not whitelisted. + 97 : : * @param spender The delegated spender address. + 98 : : * @param from The sender address. + 99 : : * @param to The recipient address. + 100 : : * @return The restriction code, or TRANSFER_OK when allowed. + 101 : : */ + 102 : 35 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256) + 103 : : internal + 104 : : view + 105 : : virtual + 106 : : override + 107 : : returns (uint8) + 108 : : { + 109 : : // Mint (from == address(0)) and burn (to == address(0)) are exempt from the spender check: + 110 : : // the minter/burner acts on its own authority, not as a delegated ERC-20 spender. + 111 [ + ]: 35 : if (from != address(0) && to != address(0) && !_isAddressListed(spender)) { + 112 : 13 : return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; + 113 : : } + 114 : 22 : return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 115 : : } + 116 : : + 117 : : /** + 118 : : * @notice No-op: regular transfers are intentionally ignored by this rule. + 119 : : */ + 120 : 9 : function _transferred(address, address, uint256) internal view virtual override { + 121 : : // no-op: regular transfers are intentionally ignored by this rule + 122 : : } + 123 : : + 124 : : /** + 125 : : * @notice Reverts if a delegated transfer is blocked because the spender is not whitelisted. + 126 : : * @param spender The delegated spender address. + 127 : : * @param from The sender address. + 128 : : * @param to The recipient address. + 129 : : * @param value The amount transferred. + 130 : : */ + 131 : 17 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { + 132 : 17 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); + 133 [ + + ]: 17 : require( + 134 : : code == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK), + 135 : : RuleSpenderWhitelist_InvalidTransferFrom(address(this), spender, from, to, value, code) + 136 : : ); + 137 : : } + 138 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistBase.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistBase.sol.func-sort-c.html index af592e2..a5012cb 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistBase.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistBase.sol.func-sort-c.html @@ -31,13 +31,13 @@ lcov.info Lines: - 26 - 27 - 96.3 % + 30 + 31 + 96.8 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 8 @@ -49,8 +49,8 @@ Branches: - 5 - 5 + 4 + 4 100.0 % @@ -69,40 +69,40 @@ Hit count Sort by hit count - RuleWhitelistBase._authorizeCheckSpenderManager + RuleWhitelistBase._authorizeCheckSpenderManager 0 - RuleWhitelistBase._setCheckSpender + RuleWhitelistBase._setCheckSpender 2 - RuleWhitelistBase.isVerified - 2 + RuleWhitelistBase.onlyCheckSpenderManager + 3 - RuleWhitelistBase.onlyCheckSpenderManager + RuleWhitelistBase.setCheckSpender 3 - RuleWhitelistBase.setCheckSpender - 3 + RuleWhitelistBase.isVerified + 6 - RuleWhitelistBase._detectTransferRestrictionFrom - 18 + RuleWhitelistBase.supportsInterface + 35 - RuleWhitelistBase.supportsInterface - 27 + RuleWhitelistBase._detectTransferRestrictionFrom + 38 - RuleWhitelistBase._detectTransferRestriction - 53 + RuleWhitelistBase._detectTransferRestriction + 100 - RuleWhitelistBase.constructor - 163 + RuleWhitelistBase.constructor + 188
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistBase.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistBase.sol.func.html index b092a21..93e28b1 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistBase.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistBase.sol.func.html @@ -31,13 +31,13 @@ lcov.info Lines: - 26 - 27 - 96.3 % + 30 + 31 + 96.8 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 8 @@ -49,8 +49,8 @@ Branches: - 5 - 5 + 4 + 4 100.0 % @@ -69,40 +69,40 @@ Hit count Sort by hit count - RuleWhitelistBase._authorizeCheckSpenderManager + RuleWhitelistBase._authorizeCheckSpenderManager 0 - RuleWhitelistBase._detectTransferRestriction - 53 + RuleWhitelistBase._detectTransferRestriction + 100 - RuleWhitelistBase._detectTransferRestrictionFrom - 18 + RuleWhitelistBase._detectTransferRestrictionFrom + 38 - RuleWhitelistBase._setCheckSpender + RuleWhitelistBase._setCheckSpender 2 - RuleWhitelistBase.constructor - 163 + RuleWhitelistBase.constructor + 188 - RuleWhitelistBase.isVerified - 2 + RuleWhitelistBase.isVerified + 6 - RuleWhitelistBase.onlyCheckSpenderManager + RuleWhitelistBase.onlyCheckSpenderManager 3 - RuleWhitelistBase.setCheckSpender + RuleWhitelistBase.setCheckSpender 3 - RuleWhitelistBase.supportsInterface - 27 + RuleWhitelistBase.supportsInterface + 35
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistBase.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistBase.sol.gcov.html index 58b5f4c..8dbdb3b 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistBase.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistBase.sol.gcov.html @@ -31,13 +31,13 @@ lcov.info Lines: - 26 - 27 - 96.3 % + 30 + 31 + 96.8 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 8 @@ -49,8 +49,8 @@ Branches: - 5 - 5 + 4 + 4 100.0 % @@ -76,100 +76,161 @@ 5 : : import {RuleWhitelistShared} from "../core/RuleWhitelistShared.sol"; 6 : : import {RuleTransferValidation} from "../core/RuleTransferValidation.sol"; 7 : : import {IIdentityRegistryVerified} from "../../../interfaces/IIdentityRegistry.sol"; - 8 : : - 9 : : /** - 10 : : * @title RuleWhitelistBase - 11 : : * @notice Core whitelist logic without access-control policy. - 12 : : */ - 13 : : abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIdentityRegistryVerified { - 14 : : /*////////////////////////////////////////////////////////////// - 15 : : CONSTRUCTOR - 16 : : //////////////////////////////////////////////////////////////*/ - 17 : : - 18 : 163 : constructor(address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) - 19 : : RuleAddressSet(forwarderIrrevocable) - 20 : : { - 21 : 163 : checkSpender = checkSpender_; - 22 [ + ]: 2 : if (allowMintBurn) { - 23 : 2 : _addAddress(address(0)); - 24 : 2 : emit AddAddress(address(0)); - 25 : : } - 26 : : } - 27 : : - 28 : : /*////////////////////////////////////////////////////////////// - 29 : : PUBLIC FUNCTIONS - 30 : : //////////////////////////////////////////////////////////////*/ - 31 : : - 32 : 3 : function setCheckSpender(bool value) public virtual onlyCheckSpenderManager { - 33 : 2 : _setCheckSpender(value); - 34 : 2 : emit CheckSpenderUpdated(value); - 35 : : } - 36 : : - 37 : 2 : function isVerified(address targetAddress) - 38 : : public - 39 : : view - 40 : : virtual - 41 : : override(IIdentityRegistryVerified) - 42 : : returns (bool isListed) - 43 : : { - 44 : 2 : isListed = _isAddressListed(targetAddress); - 45 : : } - 46 : : - 47 : 27 : function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { - 48 : 27 : return RuleTransferValidation.supportsInterface(interfaceId); - 49 : : } - 50 : : - 51 : : /*////////////////////////////////////////////////////////////// - 52 : : ACCESS CONTROL - 53 : : //////////////////////////////////////////////////////////////*/ - 54 : : - 55 : 3 : modifier onlyCheckSpenderManager() { - 56 : 3 : _authorizeCheckSpenderManager(); - 57 : : _; - 58 : : } - 59 : : - 60 : 0 : function _authorizeCheckSpenderManager() internal view virtual; - 61 : : - 62 : : /*////////////////////////////////////////////////////////////// - 63 : : INTERNAL FUNCTIONS - 64 : : //////////////////////////////////////////////////////////////*/ + 8 : : import {AddressListInterfaceId} from "../../../interfaces/library/AddressListInterfaceId.sol"; + 9 : : + 10 : : /** + 11 : : * @title RuleWhitelistBase + 12 : : * @notice Core whitelist logic without access-control policy. + 13 : : */ + 14 : : abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIdentityRegistryVerified { + 15 : : /*////////////////////////////////////////////////////////////// + 16 : : CONSTRUCTOR + 17 : : //////////////////////////////////////////////////////////////*/ + 18 : : + 19 : : /** + 20 : : * @notice Deploys the whitelist rule base. + 21 : : * @dev `allowMintBurn` sets BOTH {allowMint} and {allowBurn} — the common case, since mint and + 22 : : * burn are normally permitted for a whitelist rule. Use {setAllowMint} / {setAllowBurn} + 23 : : * afterwards for independent control (e.g. to permanently close issuance while still + 24 : : * allowing redemptions). + 25 : : * @dev Mint/burn permission is an explicit flag and NO LONGER whitelists `address(0)`: the zero + 26 : : * address is the ERC-20 sentinel, not a participant, and listing it made + 27 : : * `isVerified(address(0))` return `true` in violation of ERC-3643. + 28 : : * @param forwarderIrrevocable Trusted ERC-2771 forwarder address for meta-transactions. + 29 : : * @param checkSpender_ Whether to also verify the spender on delegated transfers. + 30 : : * @param allowMintBurn When true, permits both minting and burning. + 31 : : */ + 32 : 188 : constructor(address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) + 33 : : RuleAddressSet(forwarderIrrevocable) + 34 : : { + 35 : 188 : checkSpender = checkSpender_; + 36 : 188 : _setAllowMintBurn(allowMintBurn, allowMintBurn); + 37 : : } + 38 : : + 39 : : /*////////////////////////////////////////////////////////////// + 40 : : PUBLIC FUNCTIONS + 41 : : //////////////////////////////////////////////////////////////*/ + 42 : : + 43 : : /** + 44 : : * @notice Enables or disables spender verification on delegated transfers. + 45 : : * @dev Restricted to the check-spender manager; emits {CheckSpenderUpdated}. + 46 : : * @param value The new state of the `checkSpender` flag. + 47 : : */ + 48 : 3 : function setCheckSpender(bool value) public virtual onlyCheckSpenderManager { + 49 : 2 : _setCheckSpender(value); + 50 : 2 : emit CheckSpenderUpdated(value); + 51 : : } + 52 : : + 53 : : /** + 54 : : * @inheritdoc IIdentityRegistryVerified + 55 : : */ + 56 : 6 : function isVerified(address targetAddress) + 57 : : public + 58 : : view + 59 : : virtual + 60 : : override(IIdentityRegistryVerified) + 61 : : returns (bool isListed) + 62 : : { + 63 : 6 : isListed = _isAddressListed(targetAddress); + 64 : : } 65 : : - 66 : 53 : function _detectTransferRestriction( - 67 : : address from, - 68 : : address to, - 69 : : uint256 /* value */ - 70 : : ) - 71 : : internal - 72 : : view - 73 : : virtual - 74 : : override - 75 : : returns (uint8) - 76 : : { - 77 [ + + ]: 53 : if (!isAddressListed(from)) { - 78 : 13 : return CODE_ADDRESS_FROM_NOT_WHITELISTED; - 79 [ + ]: 40 : } else if (!isAddressListed(to)) { - 80 : 10 : return CODE_ADDRESS_TO_NOT_WHITELISTED; - 81 : : } - 82 : 30 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 66 : : /** + 67 : : * @inheritdoc RuleTransferValidation + 68 : : */ + 69 : 35 : function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { + 70 : : // Advertise IAddressList: this rule manages an address set and is usable as a + 71 : : // child rule of RuleWhitelistWrapper, which calls it through IAddressList. + 72 : 35 : return interfaceId == AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID + 73 : 33 : || RuleTransferValidation.supportsInterface(interfaceId); + 74 : : } + 75 : : + 76 : : /*////////////////////////////////////////////////////////////// + 77 : : ACCESS CONTROL + 78 : : //////////////////////////////////////////////////////////////*/ + 79 : : + 80 : 3 : modifier onlyCheckSpenderManager() { + 81 : 3 : _authorizeCheckSpenderManager(); + 82 : : _; 83 : : } 84 : : - 85 : 18 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - 86 : : internal - 87 : : view - 88 : : virtual - 89 : : override - 90 : : returns (uint8) - 91 : : { - 92 [ + ]: 18 : if (checkSpender && !isAddressListed(spender)) { - 93 : 8 : return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; - 94 : : } - 95 : 10 : return _detectTransferRestriction(from, to, value); - 96 : : } - 97 : : - 98 : 2 : function _setCheckSpender(bool value) internal virtual { - 99 : 2 : checkSpender = value; - 100 : : } - 101 : : } + 85 : : /*////////////////////////////////////////////////////////////// + 86 : : INTERNAL FUNCTIONS + 87 : : //////////////////////////////////////////////////////////////*/ + 88 : : + 89 : : /** + 90 : : * @notice Internal helper to update the `checkSpender` flag. + 91 : : * @param value New flag value. + 92 : : */ + 93 : 2 : function _setCheckSpender(bool value) internal virtual { + 94 : 2 : checkSpender = value; + 95 : : } + 96 : : + 97 : : /** + 98 : : * @notice Authorizes the caller as check-spender manager; reverts otherwise. + 99 : : * @dev Implemented by concrete subclasses with the desired access-control policy. + 100 : : */ + 101 : 0 : function _authorizeCheckSpenderManager() internal view virtual; + 102 : : + 103 : : /** + 104 : : * @notice Detects whether a direct transfer is restricted by the whitelist. + 105 : : * @param from The sender address. + 106 : : * @param to The recipient address. + 107 : : * @return The restriction code, or TRANSFER_OK when both parties are whitelisted. + 108 : : */ + 109 : 100 : function _detectTransferRestriction( + 110 : : address from, + 111 : : address to, + 112 : : uint256 /* value */ + 113 : : ) + 114 : : internal + 115 : : view + 116 : : virtual + 117 : : override + 118 : : returns (uint8) + 119 : : { + 120 : 100 : bool isMint = from == address(0); + 121 : 100 : bool isBurn = to == address(0); + 122 : : + 123 : : // Gate the mint/burn OPERATION explicitly, rather than by listing the zero address. + 124 : 100 : uint8 mintBurnCode = _detectMintBurnRestriction(from, to); + 125 [ + ]: 100 : if (mintBurnCode != uint8(REJECTED_CODE_BASE.TRANSFER_OK)) { + 126 : 7 : return mintBurnCode; + 127 : : } + 128 : : + 129 : : // Screen only the REAL participants. A permitted mint still requires a whitelisted + 130 : : // recipient; a permitted burn still requires a whitelisted sender. + 131 [ + ]: 93 : if (!isMint && !isAddressListed(from)) { + 132 : 30 : return CODE_ADDRESS_FROM_NOT_WHITELISTED; + 133 : : } + 134 [ + ]: 63 : if (!isBurn && !isAddressListed(to)) { + 135 : 11 : return CODE_ADDRESS_TO_NOT_WHITELISTED; + 136 : : } + 137 : 52 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 138 : : } + 139 : : + 140 : : /** + 141 : : * @notice Detects whether a delegated transfer is restricted by the whitelist. + 142 : : * @param spender The delegated spender address. + 143 : : * @param from The sender address. + 144 : : * @param to The recipient address. + 145 : : * @param value The amount transferred. + 146 : : * @return The restriction code, or TRANSFER_OK when allowed. + 147 : : */ + 148 : 38 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + 149 : : internal + 150 : : view + 151 : : virtual + 152 : : override + 153 : : returns (uint8) + 154 : : { + 155 : : // Mint (from == address(0)) and burn (to == address(0)) are exempt from the spender check: + 156 : : // the minter/burner acts on its own authority, not as a delegated ERC-20 spender. + 157 [ + ]: 38 : if (checkSpender && from != address(0) && to != address(0) && !isAddressListed(spender)) { + 158 : 8 : return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; + 159 : : } + 160 : 30 : return _detectTransferRestriction(from, to, value); + 161 : : } + 162 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol.func-sort-c.html index a659423..9951824 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol.func-sort-c.html @@ -31,26 +31,26 @@ lcov.info Lines: - 67 - 68 - 98.5 % + 87 + 88 + 98.9 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 14 15 - 93.3 % + 16 + 93.8 % Branches: - 14 - 14 + 20 + 20 100.0 % @@ -69,64 +69,68 @@ Hit count Sort by hit count - RuleWhitelistWrapperBase._authorizeCheckSpenderManager + RuleWhitelistWrapperBase._authorizeCheckSpenderManager 0 - RuleWhitelistWrapperBase._transferred.1 + RuleWhitelistWrapperBase._transferred.1 1 - RuleWhitelistWrapperBase._msgData + RuleWhitelistWrapperBase._msgData 2 - RuleWhitelistWrapperBase._setCheckSpender + RuleWhitelistWrapperBase._setCheckSpender 3 - RuleWhitelistWrapperBase.isVerified + RuleWhitelistWrapperBase.onlyCheckSpenderManager 4 - RuleWhitelistWrapperBase.onlyCheckSpenderManager + RuleWhitelistWrapperBase.setCheckSpender 4 - RuleWhitelistWrapperBase.setCheckSpender - 4 + RuleWhitelistWrapperBase._isListedInAnyChild + 5 - RuleWhitelistWrapperBase._transferred.0 - 13 + RuleWhitelistWrapperBase.isVerified + 7 - RuleWhitelistWrapperBase._detectTransferRestrictionFrom + RuleWhitelistWrapperBase._transferred.0 20 - RuleWhitelistWrapperBase._detectTransferRestriction - 34 + RuleWhitelistWrapperBase._detectTransferRestrictionFrom + 37 - RuleWhitelistWrapperBase.constructor - 44 + RuleWhitelistWrapperBase.supportsInterface + 49 - RuleWhitelistWrapperBase.supportsInterface - 46 + RuleWhitelistWrapperBase.constructor + 57 - RuleWhitelistWrapperBase._detectTransferRestrictionForTargets - 57 + RuleWhitelistWrapperBase._detectTransferRestriction + 66 + + + RuleWhitelistWrapperBase._detectTransferRestrictionForTargets + 102 - RuleWhitelistWrapperBase._msgSender - 145 + RuleWhitelistWrapperBase._msgSender + 175 - RuleWhitelistWrapperBase._contextSuffixLength - 147 + RuleWhitelistWrapperBase._contextSuffixLength + 177
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol.func.html index cecc2cd..cc5b2b9 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol.func.html @@ -31,26 +31,26 @@ lcov.info Lines: - 67 - 68 - 98.5 % + 87 + 88 + 98.9 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 14 15 - 93.3 % + 16 + 93.8 % Branches: - 14 - 14 + 20 + 20 100.0 % @@ -69,64 +69,68 @@ Hit count Sort by hit count - RuleWhitelistWrapperBase._authorizeCheckSpenderManager + RuleWhitelistWrapperBase._authorizeCheckSpenderManager 0 - RuleWhitelistWrapperBase._contextSuffixLength - 147 + RuleWhitelistWrapperBase._contextSuffixLength + 177 - RuleWhitelistWrapperBase._detectTransferRestriction - 34 + RuleWhitelistWrapperBase._detectTransferRestriction + 66 - RuleWhitelistWrapperBase._detectTransferRestrictionForTargets - 57 + RuleWhitelistWrapperBase._detectTransferRestrictionForTargets + 102 - RuleWhitelistWrapperBase._detectTransferRestrictionFrom - 20 + RuleWhitelistWrapperBase._detectTransferRestrictionFrom + 37 + + + RuleWhitelistWrapperBase._isListedInAnyChild + 5 - RuleWhitelistWrapperBase._msgData + RuleWhitelistWrapperBase._msgData 2 - RuleWhitelistWrapperBase._msgSender - 145 + RuleWhitelistWrapperBase._msgSender + 175 - RuleWhitelistWrapperBase._setCheckSpender + RuleWhitelistWrapperBase._setCheckSpender 3 - RuleWhitelistWrapperBase._transferred.0 - 13 + RuleWhitelistWrapperBase._transferred.0 + 20 - RuleWhitelistWrapperBase._transferred.1 + RuleWhitelistWrapperBase._transferred.1 1 - RuleWhitelistWrapperBase.constructor - 44 + RuleWhitelistWrapperBase.constructor + 57 - RuleWhitelistWrapperBase.isVerified - 4 + RuleWhitelistWrapperBase.isVerified + 7 - RuleWhitelistWrapperBase.onlyCheckSpenderManager + RuleWhitelistWrapperBase.onlyCheckSpenderManager 4 - RuleWhitelistWrapperBase.setCheckSpender + RuleWhitelistWrapperBase.setCheckSpender 4 - RuleWhitelistWrapperBase.supportsInterface - 46 + RuleWhitelistWrapperBase.supportsInterface + 49
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol.gcov.html index 908f101..487c382 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol.gcov.html @@ -31,26 +31,26 @@ lcov.info Lines: - 67 - 68 - 98.5 % + 87 + 88 + 98.9 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 14 15 - 93.3 % + 16 + 93.8 % Branches: - 14 - 14 + 20 + 20 100.0 % @@ -97,217 +97,298 @@ 26 : : CONSTRUCTOR 27 : : //////////////////////////////////////////////////////////////*/ 28 : : /** - 29 : : * @param forwarderIrrevocable Address of the forwarder, required for the gasless support - 30 : : */ - 31 : 44 : constructor(address forwarderIrrevocable, bool checkSpender_) MetaTxModuleStandalone(forwarderIrrevocable) { - 32 : 44 : checkSpender = checkSpender_; - 33 : : } - 34 : : - 35 : : /*////////////////////////////////////////////////////////////// - 36 : : ACCESS CONTROL - 37 : : //////////////////////////////////////////////////////////////*/ - 38 : : - 39 : 4 : modifier onlyCheckSpenderManager() { - 40 : 4 : _authorizeCheckSpenderManager(); - 41 : : _; + 29 : : * @notice Deploys the whitelist wrapper base. + 30 : : * @dev The wrapper holds no addresses of its own — it ORs its child rules. It therefore needs its + 31 : : * OWN mint/burn flags: the children no longer list `address(0)`, so without these a mint + 32 : : * would resolve `from` as unlisted and be rejected. + 33 : : * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + 34 : : * @param checkSpender_ Whether to also verify the spender on delegated transfers. + 35 : : * @param allowMintBurn When true, permits both minting and burning. + 36 : : */ + 37 : 57 : constructor(address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) + 38 : : MetaTxModuleStandalone(forwarderIrrevocable) + 39 : : { + 40 : 57 : checkSpender = checkSpender_; + 41 : 57 : _setAllowMintBurn(allowMintBurn, allowMintBurn); 42 : : } 43 : : - 44 : 0 : function _authorizeCheckSpenderManager() internal virtual; - 45 : : - 46 : : /*////////////////////////////////////////////////////////////// - 47 : : PUBLIC FUNCTIONS - 48 : : //////////////////////////////////////////////////////////////*/ - 49 : : - 50 : : /** - 51 : : * @notice Sets whether the rule should enforce spender-based checks. - 52 : : * @dev - 53 : : * - Restricted to holders of the manager role. - 54 : : * - Updates the internal `checkSpender` flag. - 55 : : * - Emits a {CheckSpenderUpdated} event. - 56 : : * @param value The new state of the `checkSpender` flag. - 57 : : */ - 58 : 4 : function setCheckSpender(bool value) public virtual onlyCheckSpenderManager { - 59 : 3 : _setCheckSpender(value); - 60 : 3 : emit CheckSpenderUpdated(value); - 61 : : } - 62 : : - 63 : 46 : function supportsInterface(bytes4 interfaceId) - 64 : : public - 65 : : view - 66 : : virtual - 67 : : override(RuleTransferValidation) - 68 : : returns (bool) - 69 : : { - 70 : 46 : return RuleTransferValidation.supportsInterface(interfaceId); - 71 : : } - 72 : : - 73 : : /** - 74 : : * @notice Returns true if the address is listed in at least one child whitelist rule. - 75 : : * @dev Delegates to the same child-rule scan used by transfer restriction checks. - 76 : : */ - 77 : 4 : function isVerified(address targetAddress) public view virtual override(IIdentityRegistryVerified) returns (bool) { - 78 : 4 : address[] memory targets = new address[](1); - 79 : 4 : targets[0] = targetAddress; - 80 : 4 : bool[] memory result = _detectTransferRestrictionForTargets(targets); - 81 : 4 : return result[0]; - 82 : : } - 83 : : - 84 : : /*////////////////////////////////////////////////////////////// - 85 : : INTERNAL FUNCTIONS - 86 : : //////////////////////////////////////////////////////////////*/ - 87 : : - 88 : : /** - 89 : : * @notice Go through all the whitelist rules to know if a restriction exists on the transfer - 90 : : * @param from the origin address - 91 : : * @param to the destination address - 92 : : * @return The restricion code or REJECTED_CODE_BASE.TRANSFER_OK - 93 : : * - 94 : : */ - 95 : 34 : function _detectTransferRestriction( - 96 : : address from, - 97 : : address to, - 98 : : uint256 /* value */ - 99 : : ) - 100 : : internal - 101 : : view - 102 : : virtual - 103 : : override - 104 : : returns (uint8) - 105 : : { - 106 : 34 : address[] memory targetAddress = new address[](2); - 107 : 34 : targetAddress[0] = from; - 108 : 34 : targetAddress[1] = to; + 44 : : /*////////////////////////////////////////////////////////////// + 45 : : ACCESS CONTROL + 46 : : //////////////////////////////////////////////////////////////*/ + 47 : : + 48 : 4 : modifier onlyCheckSpenderManager() { + 49 : 4 : _authorizeCheckSpenderManager(); + 50 : : _; + 51 : : } + 52 : : + 53 : : /*////////////////////////////////////////////////////////////// + 54 : : PUBLIC FUNCTIONS + 55 : : //////////////////////////////////////////////////////////////*/ + 56 : : + 57 : : /** + 58 : : * @notice Sets whether the rule should enforce spender-based checks. + 59 : : * @dev + 60 : : * - Restricted to holders of the manager role. + 61 : : * - Updates the internal `checkSpender` flag. + 62 : : * - Emits a {CheckSpenderUpdated} event. + 63 : : * @param value The new state of the `checkSpender` flag. + 64 : : */ + 65 : 4 : function setCheckSpender(bool value) public virtual onlyCheckSpenderManager { + 66 : 3 : _setCheckSpender(value); + 67 : 3 : emit CheckSpenderUpdated(value); + 68 : : } + 69 : : + 70 : : /** + 71 : : * @inheritdoc RuleTransferValidation + 72 : : */ + 73 : 49 : function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { + 74 : 49 : return RuleTransferValidation.supportsInterface(interfaceId); + 75 : : } + 76 : : + 77 : : /** + 78 : : * @notice Returns true if the address is listed in at least one child whitelist rule. + 79 : : * @dev Delegates to the same child-rule scan used by transfer restriction checks. + 80 : : * @param targetAddress The address to check across all child whitelist rules. + 81 : : * @return True if the address is listed in at least one child rule. + 82 : : */ + 83 : 7 : function isVerified(address targetAddress) public view virtual override(IIdentityRegistryVerified) returns (bool) { + 84 : 7 : address[] memory targets = new address[](1); + 85 : 7 : targets[0] = targetAddress; + 86 : 7 : bool[] memory result = _detectTransferRestrictionForTargets(targets); + 87 : 7 : return result[0]; + 88 : : } + 89 : : + 90 : : /*////////////////////////////////////////////////////////////// + 91 : : INTERNAL FUNCTIONS + 92 : : //////////////////////////////////////////////////////////////*/ + 93 : : + 94 : : /** + 95 : : * @notice Authorizes the caller as check-spender manager; reverts otherwise. + 96 : : * @dev Implemented by concrete subclasses with the desired access-control policy. + 97 : : * `view` by convention: an access-control hook checks and reverts, it never mutates state. + 98 : : * Declaring it `view` makes that a compiler-enforced invariant rather than a convention. + 99 : : */ + 100 : 0 : function _authorizeCheckSpenderManager() internal view virtual; + 101 : : + 102 : : /** + 103 : : * @notice Internal helper to update the `checkSpender` flag. + 104 : : * @param value New flag value. + 105 : : */ + 106 : 3 : function _setCheckSpender(bool value) internal virtual { + 107 : 3 : checkSpender = value; + 108 : : } 109 : : - 110 : 34 : bool[] memory result = _detectTransferRestrictionForTargets(targetAddress); - 111 [ + + ]: 34 : if (!result[0]) { - 112 : 13 : return CODE_ADDRESS_FROM_NOT_WHITELISTED; - 113 [ + + ]: 21 : } else if (!result[1]) { - 114 : 8 : return CODE_ADDRESS_TO_NOT_WHITELISTED; - 115 : : } else { - 116 : 13 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); - 117 : : } - 118 : : } - 119 : : - 120 : 20 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - 121 : : internal - 122 : : view - 123 : : virtual - 124 : : override - 125 : : returns (uint8) - 126 : : { - 127 [ + ]: 20 : if (!checkSpender) { - 128 : 1 : return _detectTransferRestriction(from, to, value); - 129 : : } - 130 : : - 131 : 19 : address[] memory targetAddress = new address[](3); - 132 : 19 : targetAddress[0] = from; - 133 : 19 : targetAddress[1] = to; - 134 : 19 : targetAddress[2] = spender; - 135 : : - 136 : 19 : bool[] memory result = _detectTransferRestrictionForTargets(targetAddress); - 137 : : - 138 [ + + ]: 19 : if (!result[0]) { - 139 : 1 : return CODE_ADDRESS_FROM_NOT_WHITELISTED; - 140 [ + + ]: 18 : } else if (!result[1]) { - 141 : 1 : return CODE_ADDRESS_TO_NOT_WHITELISTED; - 142 [ + + ]: 17 : } else if (!result[2]) { - 143 : 8 : return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; - 144 : : } else { - 145 : 9 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); - 146 : : } - 147 : : } - 148 : : - 149 : : // ERC-7943 tokenId overloads are provided by {RuleNFTAdapter} via RuleWhitelistShared. - 150 : : - 151 : 13 : function _transferred(address from, address to, uint256 value) - 152 : : internal - 153 : : view - 154 : : virtual - 155 : : override(RulesManagementModule, RuleWhitelistShared) - 156 : : { - 157 : 13 : RuleWhitelistShared._transferred(from, to, value); - 158 : : } - 159 : : - 160 : 1 : function _transferred(address spender, address from, address to, uint256 value) - 161 : : internal - 162 : : view - 163 : : virtual - 164 : : override(RulesManagementModule) - 165 : : { - 166 : 1 : RuleWhitelistShared._transferredFrom(spender, from, to, value); - 167 : : } - 168 : : - 169 : : /** - 170 : : * @notice Evaluates target addresses across all child rules. - 171 : : * @param targetAddress Addresses to validate (from/to[/spender]). - 172 : : * @return result Boolean array aligned with targetAddress indicating if each address is listed. - 173 : : */ - 174 : 57 : function _detectTransferRestrictionForTargets(address[] memory targetAddress) - 175 : : internal - 176 : : view - 177 : : virtual - 178 : : returns (bool[] memory) - 179 : : { - 180 : 57 : uint256 rulesLength = rulesCount(); - 181 : 57 : bool[] memory result = new bool[](targetAddress.length); - 182 : 57 : for (uint256 i = 0; i < rulesLength; ++i) { - 183 : : // Call the whitelist rules - 184 : : // Gas cost grows with the number of rules. Keep the wrapper list bounded. - 185 : 105 : bool[] memory isListed = IAddressList(rule(i)).areAddressesListed(targetAddress); - 186 : 105 : for (uint256 j = 0; j < targetAddress.length; ++j) { - 187 [ + ]: 85 : if (isListed[j]) { - 188 : 85 : result[j] = true; - 189 : : } - 190 : : } - 191 : : - 192 : : // Break early if all listed - 193 : 105 : bool allListed = true; - 194 : 105 : for (uint256 k = 0; k < result.length; ++k) { - 195 [ + ]: 201 : if (!result[k]) { - 196 : 81 : allListed = false; - 197 : 81 : break; - 198 : : } - 199 : : } - 200 [ + ]: 24 : if (allListed) { - 201 : 24 : break; - 202 : : } - 203 : : } - 204 : 57 : return result; - 205 : : } - 206 : : - 207 : : /** - 208 : : * @notice Internal helper to update the `checkSpender` flag. - 209 : : * @param value New flag value. - 210 : : */ - 211 : 3 : function _setCheckSpender(bool value) internal virtual { - 212 : 3 : checkSpender = value; - 213 : : } - 214 : : - 215 : : /*////////////////////////////////////////////////////////////// - 216 : : ERC-2771 - 217 : : //////////////////////////////////////////////////////////////*/ - 218 : : - 219 : : /** - 220 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 221 : : */ - 222 : 145 : function _msgSender() internal view virtual override(ERC2771Context) returns (address sender) { - 223 : 145 : return ERC2771Context._msgSender(); - 224 : : } - 225 : : - 226 : : /** - 227 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 228 : : */ - 229 : 2 : function _msgData() internal view virtual override(ERC2771Context) returns (bytes calldata) { - 230 : 2 : return ERC2771Context._msgData(); - 231 : : } - 232 : : - 233 : : /** - 234 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule - 235 : : */ - 236 : 147 : function _contextSuffixLength() internal view virtual override(ERC2771Context) returns (uint256) { - 237 : 147 : return ERC2771Context._contextSuffixLength(); - 238 : : } - 239 : : } + 110 : : /** + 111 : : * @notice Go through all the whitelist rules to know if a restriction exists on the transfer + 112 : : * @param from the origin address + 113 : : * @param to the destination address + 114 : : * @return The restricion code or REJECTED_CODE_BASE.TRANSFER_OK + 115 : : * + 116 : : */ + 117 : 66 : function _detectTransferRestriction( + 118 : : address from, + 119 : : address to, + 120 : : uint256 /* value */ + 121 : : ) + 122 : : internal + 123 : : view + 124 : : virtual + 125 : : override + 126 : : returns (uint8) + 127 : : { + 128 : : // Gate the mint/burn OPERATION explicitly, before consulting any child rule. + 129 : 66 : uint8 mintBurnCode = _detectMintBurnRestriction(from, to); + 130 [ + ]: 66 : if (mintBurnCode != uint8(REJECTED_CODE_BASE.TRANSFER_OK)) { + 131 : 4 : return mintBurnCode; + 132 : : } + 133 : : + 134 : 62 : bool isMint = from == address(0); + 135 : 62 : bool isBurn = to == address(0); + 136 : : + 137 : : // Resolve only the REAL participants against the children: the zero address is a sentinel, + 138 : : // not a listed member of any child, so asking about it would always fail. + 139 : : // Degenerate (0, 0): neither leg is a real participant, so there is nothing to screen. + 140 : : // Handled explicitly so the wrapper and {RuleWhitelistBase} return the same answer — the two + 141 : : // share `_detectMintBurnRestriction` precisely so they cannot drift. + 142 [ + ]: 62 : if (isMint && isBurn) { + 143 : 2 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 144 : : } + 145 [ + ]: 3 : if (isMint) { + 146 [ + ]: 3 : if (!_isListedInAnyChild(to)) { + 147 : 1 : return CODE_ADDRESS_TO_NOT_WHITELISTED; + 148 : : } + 149 : 2 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 150 : : } + 151 [ + ]: 2 : if (isBurn) { + 152 [ + ]: 2 : if (!_isListedInAnyChild(from)) { + 153 : 1 : return CODE_ADDRESS_FROM_NOT_WHITELISTED; + 154 : : } + 155 : 1 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 156 : : } + 157 : : + 158 : 55 : address[] memory targetAddress = new address[](2); + 159 : 55 : targetAddress[0] = from; + 160 : 55 : targetAddress[1] = to; + 161 : : + 162 : 55 : bool[] memory result = _detectTransferRestrictionForTargets(targetAddress); + 163 [ + + ]: 54 : if (!result[0]) { + 164 : 22 : return CODE_ADDRESS_FROM_NOT_WHITELISTED; + 165 [ + + ]: 32 : } else if (!result[1]) { + 166 : 8 : return CODE_ADDRESS_TO_NOT_WHITELISTED; + 167 : : } else { + 168 : 24 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 169 : : } + 170 : : } + 171 : : + 172 : : /** + 173 : : * @notice Returns true when `targetAddress` is listed in at least one child rule. + 174 : : * @param targetAddress The address to resolve across the children. + 175 : : * @return True if listed in any child. + 176 : : */ + 177 : 5 : function _isListedInAnyChild(address targetAddress) internal view virtual returns (bool) { + 178 : 5 : address[] memory targets = new address[](1); + 179 : 5 : targets[0] = targetAddress; + 180 : 5 : return _detectTransferRestrictionForTargets(targets)[0]; + 181 : : } + 182 : : + 183 : : /** + 184 : : * @notice Go through all the whitelist rules to know if a delegated transfer is restricted. + 185 : : * @param spender The delegated spender address. + 186 : : * @param from The origin address. + 187 : : * @param to The destination address. + 188 : : * @param value The amount transferred. + 189 : : * @return The restriction code or REJECTED_CODE_BASE.TRANSFER_OK. + 190 : : */ + 191 : 37 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + 192 : : internal + 193 : : view + 194 : : virtual + 195 : : override + 196 : : returns (uint8) + 197 : : { + 198 : : // Mint (from == address(0)) and burn (to == address(0)) are exempt from the spender check: + 199 : : // the minter/burner acts on its own authority, not as a delegated ERC-20 spender. + 200 [ + ]: 37 : if (!checkSpender || from == address(0) || to == address(0)) { + 201 : 2 : return _detectTransferRestriction(from, to, value); + 202 : : } + 203 : : + 204 : 35 : address[] memory targetAddress = new address[](3); + 205 : 35 : targetAddress[0] = from; + 206 : 35 : targetAddress[1] = to; + 207 : 35 : targetAddress[2] = spender; + 208 : : + 209 : 35 : bool[] memory result = _detectTransferRestrictionForTargets(targetAddress); + 210 : : + 211 [ + + ]: 35 : if (!result[0]) { + 212 : 9 : return CODE_ADDRESS_FROM_NOT_WHITELISTED; + 213 [ + + ]: 26 : } else if (!result[1]) { + 214 : 1 : return CODE_ADDRESS_TO_NOT_WHITELISTED; + 215 [ + + ]: 25 : } else if (!result[2]) { + 216 : 8 : return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; + 217 : : } else { + 218 : 17 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 219 : : } + 220 : : } + 221 : : + 222 : : // ERC-7943 tokenId overloads are provided by {RuleNFTAdapter} via RuleWhitelistShared. + 223 : : + 224 : : /** + 225 : : * @notice Reverts if a direct transfer is blocked by any child whitelist rule. + 226 : : * @param from The sender address. + 227 : : * @param to The recipient address. + 228 : : * @param value The amount transferred. + 229 : : */ + 230 : 20 : function _transferred(address from, address to, uint256 value) + 231 : : internal + 232 : : view + 233 : : virtual + 234 : : override(RulesManagementModule, RuleWhitelistShared) + 235 : : { + 236 : 20 : RuleWhitelistShared._transferred(from, to, value); + 237 : : } + 238 : : + 239 : : /** + 240 : : * @notice Reverts if a delegated transfer is blocked by any child whitelist rule. + 241 : : * @param spender The delegated spender address. + 242 : : * @param from The sender address. + 243 : : * @param to The recipient address. + 244 : : * @param value The amount transferred. + 245 : : */ + 246 : 1 : function _transferred(address spender, address from, address to, uint256 value) + 247 : : internal + 248 : : view + 249 : : virtual + 250 : : override(RulesManagementModule) + 251 : : { + 252 : 1 : RuleWhitelistShared._transferredFrom(spender, from, to, value); + 253 : : } + 254 : : + 255 : : /** + 256 : : * @notice Evaluates target addresses across all child rules. + 257 : : * @param targetAddress Addresses to validate (from/to[/spender]). + 258 : : * @return result Boolean array aligned with targetAddress indicating if each address is listed. + 259 : : */ + 260 : 102 : function _detectTransferRestrictionForTargets(address[] memory targetAddress) + 261 : : internal + 262 : : view + 263 : : virtual + 264 : : returns (bool[] memory) + 265 : : { + 266 : 102 : uint256 rulesLength = rulesCount(); + 267 : 102 : bool[] memory result = new bool[](targetAddress.length); + 268 : 102 : for (uint256 i = 0; i < rulesLength; ++i) { + 269 : : // Call the whitelist rules + 270 : : // Gas cost grows with the number of rules. Keep the wrapper list bounded. + 271 : 153 : bool[] memory isListed = IAddressList(rule(i)).areAddressesListed(targetAddress); + 272 : 152 : for (uint256 j = 0; j < targetAddress.length; ++j) { + 273 [ + ]: 160 : if (isListed[j]) { + 274 : 160 : result[j] = true; + 275 : : } + 276 : : } + 277 : : + 278 : : // Break early if all listed + 279 : 152 : bool allListed = true; + 280 : 152 : for (uint256 k = 0; k < result.length; ++k) { + 281 [ + ]: 278 : if (!result[k]) { + 282 : 105 : allListed = false; + 283 : 105 : break; + 284 : : } + 285 : : } + 286 [ + ]: 47 : if (allListed) { + 287 : 47 : break; + 288 : : } + 289 : : } + 290 : 101 : return result; + 291 : : } + 292 : : + 293 : : /*////////////////////////////////////////////////////////////// + 294 : : ERC-2771 + 295 : : //////////////////////////////////////////////////////////////*/ + 296 : : + 297 : : /** + 298 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 299 : : * @return sender The effective message sender, unwrapped from the meta-transaction if present. + 300 : : */ + 301 : 175 : function _msgSender() internal view virtual override(ERC2771Context) returns (address sender) { + 302 : 175 : return ERC2771Context._msgSender(); + 303 : : } + 304 : : + 305 : : /** + 306 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 307 : : * @return The effective calldata, unwrapped from the meta-transaction if present. + 308 : : */ + 309 : 2 : function _msgData() internal view virtual override(ERC2771Context) returns (bytes calldata) { + 310 : 2 : return ERC2771Context._msgData(); + 311 : : } + 312 : : + 313 : : /** + 314 : : * @dev This surcharge is not necessary if you do not use the MetaTxModule + 315 : : * @return The length of the ERC-2771 context suffix appended to calldata. + 316 : : */ + 317 : 177 : function _contextSuffixLength() internal view virtual override(ERC2771Context) returns (uint256) { + 318 : 177 : return ERC2771Context._contextSuffixLength(); + 319 : : } + 320 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/index-sort-b.html b/doc/coverage/coverage/src/rules/validation/abstract/base/index-sort-b.html index f462f1b..f7db9bb 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/index-sort-b.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/index-sort-b.html @@ -31,26 +31,26 @@ lcov.info Lines: - 385 - 394 - 97.7 % + 447 + 457 + 97.8 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 110 - 119 - 92.4 % + 117 + 127 + 92.1 % Branches: - 111 - 111 + 124 + 124 100.0 % @@ -87,23 +87,23 @@
100.0%
100.0 % - 19 / 19 + 22 / 22 100.0 % - 8 / 8 + 9 / 9 100.0 % 4 / 4 RuleWhitelistBase.sol -
96.3%96.3%
+
96.8%96.8%
- 96.3 % - 26 / 27 + 96.8 % + 30 / 31 88.9 % 8 / 9 100.0 % - 5 / 5 + 4 / 4 RuleMaxTotalSupplyBase.sol @@ -117,25 +117,13 @@ 100.0 % 11 / 11 - - RuleWhitelistWrapperBase.sol - -
98.5%98.5%
- - 98.5 % - 67 / 68 - 93.3 % - 14 / 15 - 100.0 % - 14 / 14 - RuleBlacklistBase.sol
100.0%
100.0 % - 33 / 33 + 34 / 34 100.0 % 9 / 9 100.0 % @@ -156,26 +144,38 @@ RuleIdentityRegistryBase.sol -
98.1%98.1%
+
98.4%98.4%
- 98.1 % - 51 / 52 - 92.3 % - 12 / 13 + 98.4 % + 63 / 64 + 93.3 % + 14 / 15 100.0 % 19 / 19 + + RuleWhitelistWrapperBase.sol + +
98.9%98.9%
+ + 98.9 % + 87 / 88 + 93.8 % + 15 / 16 + 100.0 % + 20 / 20 + RuleERC2980Base.sol -
96.3%96.3%
+
96.2%96.2%
- 96.3 % - 105 / 109 - 89.5 % - 34 / 38 + 96.2 % + 127 / 132 + 88.1 % + 37 / 42 100.0 % - 26 / 26 + 34 / 34 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/index-sort-f.html b/doc/coverage/coverage/src/rules/validation/abstract/base/index-sort-f.html index 9f69cd4..e99a4a3 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/index-sort-f.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/index-sort-f.html @@ -31,26 +31,26 @@ lcov.info Lines: - 385 - 394 - 97.7 % + 447 + 457 + 97.8 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 110 - 119 - 92.4 % + 117 + 127 + 92.1 % Branches: - 111 - 111 + 124 + 124 100.0 % @@ -82,28 +82,28 @@ Branches Sort by branch coverage - RuleWhitelistBase.sol + RuleERC2980Base.sol -
96.3%96.3%
+
96.2%96.2%
- 96.3 % - 26 / 27 - 88.9 % - 8 / 9 + 96.2 % + 127 / 132 + 88.1 % + 37 / 42 100.0 % - 5 / 5 + 34 / 34 - RuleERC2980Base.sol + RuleWhitelistBase.sol -
96.3%96.3%
+
96.8%96.8%
- 96.3 % - 105 / 109 - 89.5 % - 34 / 38 + 96.8 % + 30 / 31 + 88.9 % + 8 / 9 100.0 % - 26 / 26 + 4 / 4 RuleMaxTotalSupplyBase.sol @@ -117,18 +117,6 @@ 100.0 % 11 / 11 - - RuleIdentityRegistryBase.sol - -
98.1%98.1%
- - 98.1 % - 51 / 52 - 92.3 % - 12 / 13 - 100.0 % - 19 / 19 - RuleSanctionsListBase.sol @@ -142,40 +130,52 @@ 18 / 18 - RuleWhitelistWrapperBase.sol + RuleIdentityRegistryBase.sol -
98.5%98.5%
+
98.4%98.4%
- 98.5 % - 67 / 68 + 98.4 % + 63 / 64 93.3 % 14 / 15 100.0 % - 14 / 14 + 19 / 19 - RuleSpenderWhitelistBase.sol + RuleWhitelistWrapperBase.sol + +
98.9%98.9%
+ + 98.9 % + 87 / 88 + 93.8 % + 15 / 16 + 100.0 % + 20 / 20 + + + RuleBlacklistBase.sol
100.0%
100.0 % - 19 / 19 + 34 / 34 100.0 % - 8 / 8 + 9 / 9 100.0 % - 4 / 4 + 14 / 14 - RuleBlacklistBase.sol + RuleSpenderWhitelistBase.sol
100.0%
100.0 % - 33 / 33 + 22 / 22 100.0 % 9 / 9 100.0 % - 14 / 14 + 4 / 4 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/index-sort-l.html b/doc/coverage/coverage/src/rules/validation/abstract/base/index-sort-l.html index a565721..210159a 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/index-sort-l.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/index-sort-l.html @@ -31,26 +31,26 @@ lcov.info Lines: - 385 - 394 - 97.7 % + 447 + 457 + 97.8 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 110 - 119 - 92.4 % + 117 + 127 + 92.1 % Branches: - 111 - 111 + 124 + 124 100.0 % @@ -82,28 +82,28 @@ Branches Sort by branch coverage - RuleWhitelistBase.sol + RuleERC2980Base.sol -
96.3%96.3%
+
96.2%96.2%
- 96.3 % - 26 / 27 - 88.9 % - 8 / 9 + 96.2 % + 127 / 132 + 88.1 % + 37 / 42 100.0 % - 5 / 5 + 34 / 34 - RuleERC2980Base.sol + RuleWhitelistBase.sol -
96.3%96.3%
+
96.8%96.8%
- 96.3 % - 105 / 109 - 89.5 % - 34 / 38 + 96.8 % + 30 / 31 + 88.9 % + 8 / 9 100.0 % - 26 / 26 + 4 / 4 RuleMaxTotalSupplyBase.sol @@ -132,26 +132,26 @@ RuleIdentityRegistryBase.sol -
98.1%98.1%
+
98.4%98.4%
- 98.1 % - 51 / 52 - 92.3 % - 12 / 13 + 98.4 % + 63 / 64 + 93.3 % + 14 / 15 100.0 % 19 / 19 RuleWhitelistWrapperBase.sol -
98.5%98.5%
+
98.9%98.9%
- 98.5 % - 67 / 68 - 93.3 % - 14 / 15 + 98.9 % + 87 / 88 + 93.8 % + 15 / 16 100.0 % - 14 / 14 + 20 / 20 RuleSpenderWhitelistBase.sol @@ -159,9 +159,9 @@
100.0%
100.0 % - 19 / 19 + 22 / 22 100.0 % - 8 / 8 + 9 / 9 100.0 % 4 / 4 @@ -171,7 +171,7 @@
100.0%
100.0 % - 33 / 33 + 34 / 34 100.0 % 9 / 9 100.0 % diff --git a/doc/coverage/coverage/src/rules/validation/abstract/base/index.html b/doc/coverage/coverage/src/rules/validation/abstract/base/index.html index 23e1e26..827de9b 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/base/index.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/base/index.html @@ -31,26 +31,26 @@ lcov.info Lines: - 385 - 394 - 97.7 % + 447 + 457 + 97.8 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 110 - 119 - 92.4 % + 117 + 127 + 92.1 % Branches: - 111 - 111 + 124 + 124 100.0 % @@ -87,7 +87,7 @@
100.0%
100.0 % - 33 / 33 + 34 / 34 100.0 % 9 / 9 100.0 % @@ -96,24 +96,24 @@ RuleERC2980Base.sol -
96.3%96.3%
+
96.2%96.2%
- 96.3 % - 105 / 109 - 89.5 % - 34 / 38 + 96.2 % + 127 / 132 + 88.1 % + 37 / 42 100.0 % - 26 / 26 + 34 / 34 RuleIdentityRegistryBase.sol -
98.1%98.1%
+
98.4%98.4%
- 98.1 % - 51 / 52 - 92.3 % - 12 / 13 + 98.4 % + 63 / 64 + 93.3 % + 14 / 15 100.0 % 19 / 19 @@ -147,35 +147,35 @@
100.0%
100.0 % - 19 / 19 + 22 / 22 100.0 % - 8 / 8 + 9 / 9 100.0 % 4 / 4 RuleWhitelistBase.sol -
96.3%96.3%
+
96.8%96.8%
- 96.3 % - 26 / 27 + 96.8 % + 30 / 31 88.9 % 8 / 9 100.0 % - 5 / 5 + 4 / 4 RuleWhitelistWrapperBase.sol -
98.5%98.5%
+
98.9%98.9%
- 98.5 % - 67 / 68 - 93.3 % - 14 / 15 + 98.9 % + 87 / 88 + 93.8 % + 15 / 16 100.0 % - 14 / 14 + 20 / 20 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleNFTAdapter.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleNFTAdapter.sol.func-sort-c.html index bdf9e03..4b427ac 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleNFTAdapter.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleNFTAdapter.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 8 @@ -69,44 +69,44 @@ Hit count Sort by hit count - RuleNFTAdapter._transferred + RuleNFTAdapter._transferred 0 - RuleNFTAdapter._transferredFrom + RuleNFTAdapter._transferredFrom 0 - RuleNFTAdapter.transferred.2 - 6 + RuleNFTAdapter.transferred.3 + 23 - RuleNFTAdapter.transferred.3 - 6 + RuleNFTAdapter.canTransferFrom + 25 - RuleNFTAdapter.transferred.1 - 9 + RuleNFTAdapter.detectTransferRestrictionFrom + 27 - RuleNFTAdapter.canTransferFrom - 11 + RuleNFTAdapter.transferred.2 + 28 - RuleNFTAdapter.detectTransferRestrictionFrom - 13 + RuleNFTAdapter.canTransfer + 29 - RuleNFTAdapter.transferred.0 - 14 + RuleNFTAdapter.detectTransferRestriction + 31 - RuleNFTAdapter.canTransfer - 15 + RuleNFTAdapter.transferred.0 + 34 - RuleNFTAdapter.detectTransferRestriction - 17 + RuleNFTAdapter.transferred.1 + 36
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleNFTAdapter.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleNFTAdapter.sol.func.html index e2a7d04..ed01ccc 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleNFTAdapter.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleNFTAdapter.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 8 @@ -69,44 +69,44 @@ Hit count Sort by hit count - RuleNFTAdapter._transferred + RuleNFTAdapter._transferred 0 - RuleNFTAdapter._transferredFrom + RuleNFTAdapter._transferredFrom 0 - RuleNFTAdapter.canTransfer - 15 + RuleNFTAdapter.canTransfer + 29 - RuleNFTAdapter.canTransferFrom - 11 + RuleNFTAdapter.canTransferFrom + 25 - RuleNFTAdapter.detectTransferRestriction - 17 + RuleNFTAdapter.detectTransferRestriction + 31 - RuleNFTAdapter.detectTransferRestrictionFrom - 13 + RuleNFTAdapter.detectTransferRestrictionFrom + 27 - RuleNFTAdapter.transferred.0 - 14 + RuleNFTAdapter.transferred.0 + 34 - RuleNFTAdapter.transferred.1 - 9 + RuleNFTAdapter.transferred.1 + 36 - RuleNFTAdapter.transferred.2 - 6 + RuleNFTAdapter.transferred.2 + 28 - RuleNFTAdapter.transferred.3 - 6 + RuleNFTAdapter.transferred.3 + 23
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleNFTAdapter.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleNFTAdapter.sol.gcov.html index e069990..adc64bd 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleNFTAdapter.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleNFTAdapter.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 8 @@ -88,160 +88,189 @@ 17 : : * @dev Delegates tokenId overloads to RuleTransferValidation's internal hooks. 18 : : */ 19 : : abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleComplianceExtend, ITransferContext { - 20 : : bytes4 internal constant TRANSFERRED_SELECTOR_ERC3643 = IERC3643IComplianceContract.transferred.selector; - 21 : : bytes4 internal constant TRANSFERRED_SELECTOR_RULE_ENGINE = IRuleEngine.transferred.selector; - 22 : : bytes4 internal constant TRANSFERRED_SELECTOR_ERC7943 = - 23 : : bytes4(keccak256("transferred(address,address,uint256,uint256)")); - 24 : : bytes4 internal constant TRANSFERRED_SELECTOR_ERC7943_FROM = - 25 : : bytes4(keccak256("transferred(address,address,address,uint256,uint256)")); - 26 : : - 27 : : /*////////////////////////////////////////////////////////////// - 28 : : PUBLIC FUNCTIONS - 29 : : //////////////////////////////////////////////////////////////*/ - 30 : : - 31 : : /** - 32 : : * @inheritdoc IERC7943NonFungibleComplianceExtend - 33 : : */ - 34 : 17 : function detectTransferRestriction( - 35 : : address from, - 36 : : address to, - 37 : : uint256, - 38 : : /* tokenId */ - 39 : : uint256 value - 40 : : ) - 41 : : public - 42 : : view - 43 : : virtual - 44 : : override(IERC7943NonFungibleComplianceExtend) - 45 : : returns (uint8) - 46 : : { - 47 : 17 : return _detectTransferRestriction(from, to, value); - 48 : : } - 49 : : - 50 : : /** - 51 : : * @inheritdoc IERC7943NonFungibleComplianceExtend - 52 : : */ - 53 : 13 : function detectTransferRestrictionFrom( - 54 : : address spender, - 55 : : address from, - 56 : : address to, - 57 : : uint256, - 58 : : /* tokenId */ - 59 : : uint256 value - 60 : : ) public view virtual override(IERC7943NonFungibleComplianceExtend) returns (uint8) { - 61 : 13 : return _detectTransferRestrictionFrom(spender, from, to, value); - 62 : : } - 63 : : - 64 : : /** - 65 : : * @inheritdoc IERC7943NonFungibleCompliance - 66 : : */ - 67 : 15 : function canTransfer( - 68 : : address from, - 69 : : address to, - 70 : : uint256, - 71 : : /* tokenId */ - 72 : : uint256 amount - 73 : : ) - 74 : : public - 75 : : view - 76 : : override(IERC7943NonFungibleCompliance) - 77 : : returns (bool) - 78 : : { - 79 : 15 : return _detectTransferRestriction(from, to, amount) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 80 : : } - 81 : : - 82 : : /** - 83 : : * @inheritdoc IERC7943NonFungibleComplianceExtend - 84 : : */ - 85 : 11 : function canTransferFrom( - 86 : : address spender, - 87 : : address from, - 88 : : address to, - 89 : : uint256, - 90 : : /* tokenId */ - 91 : : uint256 value - 92 : : ) - 93 : : public - 94 : : view - 95 : : virtual - 96 : : override(IERC7943NonFungibleComplianceExtend) - 97 : : returns (bool) - 98 : : { - 99 : 11 : return _detectTransferRestrictionFrom(spender, from, to, value) - 100 : : == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 101 : : } - 102 : : - 103 : : /** - 104 : : * @inheritdoc IERC7943NonFungibleComplianceExtend - 105 : : */ - 106 : 14 : function transferred( - 107 : : address from, - 108 : : address to, - 109 : : uint256, - 110 : : /* tokenId */ - 111 : : uint256 value - 112 : : ) - 113 : : public - 114 : : virtual - 115 : : override(IERC7943NonFungibleComplianceExtend) - 116 : : { - 117 : 14 : _transferred(from, to, value); - 118 : : } - 119 : : - 120 : : /** - 121 : : * @inheritdoc IERC7943NonFungibleComplianceExtend - 122 : : */ - 123 : 9 : function transferred( - 124 : : address spender, - 125 : : address from, - 126 : : address to, - 127 : : uint256, - 128 : : /* tokenId */ - 129 : : uint256 value - 130 : : ) - 131 : : public - 132 : : virtual - 133 : : override(IERC7943NonFungibleComplianceExtend) - 134 : : { - 135 : 9 : _transferredFrom(spender, from, to, value); - 136 : : } - 137 : : - 138 : : /** - 139 : : * @inheritdoc ITransferContext - 140 : : */ - 141 : 6 : function transferred(MultiTokenTransferContext calldata ctx) external virtual override { - 142 [ + + ]: 6 : if (ctx.sender != address(0) && ctx.sender != ctx.from) { - 143 : 3 : _transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value); - 144 : : } else { - 145 : 3 : _transferred(ctx.from, ctx.to, ctx.value); - 146 : : } - 147 : : } - 148 : : - 149 : : /** - 150 : : * @inheritdoc ITransferContext - 151 : : */ - 152 : 6 : function transferred(FungibleTransferContext calldata ctx) external virtual override { - 153 [ + + ]: 6 : if (ctx.sender != address(0) && ctx.sender != ctx.from) { - 154 : 3 : _transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value); - 155 : : } else { - 156 : 3 : _transferred(ctx.from, ctx.to, ctx.value); - 157 : : } - 158 : : } - 159 : : - 160 : : /*////////////////////////////////////////////////////////////// - 161 : : INTERNAL FUNCTIONS - 162 : : //////////////////////////////////////////////////////////////*/ - 163 : : - 164 : : /** - 165 : : * @notice Internal hook for post-transfer validation or state updates. - 166 : : */ - 167 : 0 : function _transferred(address from, address to, uint256 value) internal virtual; - 168 : : - 169 : : /** - 170 : : * @notice Internal hook for post-transfer validation or state updates (spender-aware). - 171 : : */ - 172 : 0 : function _transferredFrom(address spender, address from, address to, uint256 value) internal virtual; - 173 : : } + 20 : : /** + 21 : : * @notice Selector of the ERC-3643 compliance `transferred` hook. + 22 : : */ + 23 : : bytes4 internal constant TRANSFERRED_SELECTOR_ERC3643 = IERC3643IComplianceContract.transferred.selector; + 24 : : /** + 25 : : * @notice Selector of the RuleEngine `transferred` hook. + 26 : : */ + 27 : : bytes4 internal constant TRANSFERRED_SELECTOR_RULE_ENGINE = IRuleEngine.transferred.selector; + 28 : : /** + 29 : : * @notice Selector of the ERC-7943 `transferred(from,to,tokenId,value)` hook. + 30 : : */ + 31 : : bytes4 internal constant TRANSFERRED_SELECTOR_ERC7943 = + 32 : : bytes4(keccak256("transferred(address,address,uint256,uint256)")); + 33 : : /** + 34 : : * @notice Selector of the ERC-7943 `transferred(spender,from,to,tokenId,value)` hook. + 35 : : */ + 36 : : bytes4 internal constant TRANSFERRED_SELECTOR_ERC7943_FROM = + 37 : : bytes4(keccak256("transferred(address,address,address,uint256,uint256)")); + 38 : : + 39 : : /*////////////////////////////////////////////////////////////// + 40 : : EXTERNAL FUNCTIONS + 41 : : //////////////////////////////////////////////////////////////*/ + 42 : : + 43 : : /** + 44 : : * @inheritdoc ITransferContext + 45 : : */ + 46 : 34 : function transferred(MultiTokenTransferContext calldata ctx) external virtual override { + 47 [ + + ]: 34 : if (ctx.sender != address(0) && ctx.sender != ctx.from) { + 48 : 17 : _transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value); + 49 : : } else { + 50 : 17 : _transferred(ctx.from, ctx.to, ctx.value); + 51 : : } + 52 : : } + 53 : : + 54 : : /** + 55 : : * @inheritdoc ITransferContext + 56 : : */ + 57 : 36 : function transferred(FungibleTransferContext calldata ctx) external virtual override { + 58 [ + + ]: 36 : if (ctx.sender != address(0) && ctx.sender != ctx.from) { + 59 : 17 : _transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value); + 60 : : } else { + 61 : 19 : _transferred(ctx.from, ctx.to, ctx.value); + 62 : : } + 63 : : } + 64 : : + 65 : : /*////////////////////////////////////////////////////////////// + 66 : : PUBLIC FUNCTIONS + 67 : : //////////////////////////////////////////////////////////////*/ + 68 : : + 69 : : /** + 70 : : * @inheritdoc IERC7943NonFungibleComplianceExtend + 71 : : */ + 72 : 28 : function transferred( + 73 : : address from, + 74 : : address to, + 75 : : uint256, + 76 : : /* tokenId */ + 77 : : uint256 value + 78 : : ) + 79 : : public + 80 : : virtual + 81 : : override(IERC7943NonFungibleComplianceExtend) + 82 : : { + 83 : 28 : _transferred(from, to, value); + 84 : : } + 85 : : + 86 : : /** + 87 : : * @inheritdoc IERC7943NonFungibleComplianceExtend + 88 : : */ + 89 : 23 : function transferred( + 90 : : address spender, + 91 : : address from, + 92 : : address to, + 93 : : uint256, + 94 : : /* tokenId */ + 95 : : uint256 value + 96 : : ) + 97 : : public + 98 : : virtual + 99 : : override(IERC7943NonFungibleComplianceExtend) + 100 : : { + 101 : 23 : _transferredFrom(spender, from, to, value); + 102 : : } + 103 : : + 104 : : /** + 105 : : * @inheritdoc IERC7943NonFungibleComplianceExtend + 106 : : */ + 107 : 31 : function detectTransferRestriction( + 108 : : address from, + 109 : : address to, + 110 : : uint256, + 111 : : /* tokenId */ + 112 : : uint256 value + 113 : : ) + 114 : : public + 115 : : view + 116 : : virtual + 117 : : override(IERC7943NonFungibleComplianceExtend) + 118 : : returns (uint8) + 119 : : { + 120 : 31 : return _detectTransferRestriction(from, to, value); + 121 : : } + 122 : : + 123 : : /** + 124 : : * @inheritdoc IERC7943NonFungibleComplianceExtend + 125 : : */ + 126 : 27 : function detectTransferRestrictionFrom( + 127 : : address spender, + 128 : : address from, + 129 : : address to, + 130 : : uint256, + 131 : : /* tokenId */ + 132 : : uint256 value + 133 : : ) + 134 : : public + 135 : : view + 136 : : virtual + 137 : : override(IERC7943NonFungibleComplianceExtend) + 138 : : returns (uint8) + 139 : : { + 140 : 27 : return _detectTransferRestrictionFrom(spender, from, to, value); + 141 : : } + 142 : : + 143 : : /** + 144 : : * @inheritdoc IERC7943NonFungibleCompliance + 145 : : */ + 146 : 29 : function canTransfer( + 147 : : address from, + 148 : : address to, + 149 : : uint256, + 150 : : /* tokenId */ + 151 : : uint256 amount + 152 : : ) + 153 : : public + 154 : : view + 155 : : override(IERC7943NonFungibleCompliance) + 156 : : returns (bool) + 157 : : { + 158 : 29 : return _detectTransferRestriction(from, to, amount) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 159 : : } + 160 : : + 161 : : /** + 162 : : * @inheritdoc IERC7943NonFungibleComplianceExtend + 163 : : */ + 164 : 25 : function canTransferFrom( + 165 : : address spender, + 166 : : address from, + 167 : : address to, + 168 : : uint256, + 169 : : /* tokenId */ + 170 : : uint256 value + 171 : : ) + 172 : : public + 173 : : view + 174 : : virtual + 175 : : override(IERC7943NonFungibleComplianceExtend) + 176 : : returns (bool) + 177 : : { + 178 : 25 : return _detectTransferRestrictionFrom(spender, from, to, value) + 179 : : == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 180 : : } + 181 : : + 182 : : /*////////////////////////////////////////////////////////////// + 183 : : INTERNAL FUNCTIONS + 184 : : //////////////////////////////////////////////////////////////*/ + 185 : : + 186 : : /** + 187 : : * @notice Internal hook for post-transfer validation or state updates. + 188 : : * @param from Address tokens are transferred from. + 189 : : * @param to Address tokens are transferred to. + 190 : : * @param value Amount transferred. + 191 : : */ + 192 : 0 : function _transferred(address from, address to, uint256 value) internal virtual; + 193 : : + 194 : : /** + 195 : : * @notice Internal hook for post-transfer validation or state updates (spender-aware). + 196 : : * @param spender Address executing the transfer on behalf of `from`. + 197 : : * @param from Address tokens are transferred from. + 198 : : * @param to Address tokens are transferred to. + 199 : : * @param value Amount transferred. + 200 : : */ + 201 : 0 : function _transferredFrom(address spender, address from, address to, uint256 value) internal virtual; + 202 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleTransferValidation.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleTransferValidation.sol.func-sort-c.html index 691709b..98ecf64 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleTransferValidation.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleTransferValidation.sol.func-sort-c.html @@ -31,13 +31,13 @@ lcov.info Lines: - 14 - 16 - 87.5 % + 13 + 15 + 86.7 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 5 @@ -69,32 +69,32 @@ Hit count Sort by hit count - RuleTransferValidation._detectTransferRestriction + RuleTransferValidation._detectTransferRestriction 0 - RuleTransferValidation._detectTransferRestrictionFrom + RuleTransferValidation._detectTransferRestrictionFrom 0 - RuleTransferValidation.canTransferFrom - 14 + RuleTransferValidation.canTransferFrom + 30 - RuleTransferValidation.canTransfer - 21 + RuleTransferValidation.canTransfer + 37 - RuleTransferValidation.detectTransferRestrictionFrom - 38 + RuleTransferValidation.detectTransferRestrictionFrom + 58 - RuleTransferValidation.supportsInterface - 198 + RuleTransferValidation.supportsInterface + 230 - RuleTransferValidation.detectTransferRestriction - 351 + RuleTransferValidation.detectTransferRestriction + 923
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleTransferValidation.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleTransferValidation.sol.func.html index 8610b70..514ce56 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleTransferValidation.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleTransferValidation.sol.func.html @@ -31,13 +31,13 @@ lcov.info Lines: - 14 - 16 - 87.5 % + 13 + 15 + 86.7 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 5 @@ -69,32 +69,32 @@ Hit count Sort by hit count - RuleTransferValidation._detectTransferRestriction + RuleTransferValidation._detectTransferRestriction 0 - RuleTransferValidation._detectTransferRestrictionFrom + RuleTransferValidation._detectTransferRestrictionFrom 0 - RuleTransferValidation.canTransfer - 21 + RuleTransferValidation.canTransfer + 37 - RuleTransferValidation.canTransferFrom - 14 + RuleTransferValidation.canTransferFrom + 30 - RuleTransferValidation.detectTransferRestriction - 351 + RuleTransferValidation.detectTransferRestriction + 923 - RuleTransferValidation.detectTransferRestrictionFrom - 38 + RuleTransferValidation.detectTransferRestrictionFrom + 58 - RuleTransferValidation.supportsInterface - 198 + RuleTransferValidation.supportsInterface + 230
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleTransferValidation.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleTransferValidation.sol.gcov.html index b207db3..b36cf0a 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleTransferValidation.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleTransferValidation.sol.gcov.html @@ -31,13 +31,13 @@ lcov.info Lines: - 14 - 16 - 87.5 % + 13 + 15 + 86.7 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 5 @@ -86,113 +86,121 @@ 15 : : /* ==== Modules === */ 16 : : import {VersionModule} from "../../../../modules/VersionModule.sol"; 17 : : - 18 : : abstract contract RuleTransferValidation is - 19 : : VersionModule, - 20 : : IERC1404Extend, - 21 : : IERC3643ComplianceRead, - 22 : : IERC7551Compliance, - 23 : : IRule - 24 : : { - 25 : : /*////////////////////////////////////////////////////////////// - 26 : : PUBLIC FUNCTIONS - 27 : : //////////////////////////////////////////////////////////////*/ - 28 : : - 29 : : /** - 30 : : * @inheritdoc IERC1404 - 31 : : */ - 32 : 351 : function detectTransferRestriction(address from, address to, uint256 value) - 33 : : public - 34 : : view - 35 : : virtual - 36 : : override(IERC1404) - 37 : : returns (uint8) - 38 : : { - 39 : 351 : return _detectTransferRestriction(from, to, value); - 40 : : } - 41 : : - 42 : : /** - 43 : : * @inheritdoc IERC1404Extend - 44 : : */ - 45 : 38 : function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - 46 : : public - 47 : : view - 48 : : virtual - 49 : : override(IERC1404Extend) - 50 : : returns (uint8) - 51 : : { - 52 : 38 : return _detectTransferRestrictionFrom(spender, from, to, value); - 53 : : } - 54 : : - 55 : : /** - 56 : : * @notice Validate a transfer - 57 : : * @param from the origin address - 58 : : * @param to the destination address - 59 : : * @param amount to transfer - 60 : : * @return isValid => true if the transfer is valid, false otherwise - 61 : : * - 62 : : */ - 63 : 21 : function canTransfer(address from, address to, uint256 amount) - 64 : : public - 65 : : view - 66 : : override(IERC3643ComplianceRead) - 67 : : returns (bool isValid) - 68 : : { - 69 : 21 : return _detectTransferRestriction(from, to, amount) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 70 : : } - 71 : : - 72 : : /** - 73 : : * @inheritdoc IERC7551Compliance - 74 : : */ - 75 : 14 : function canTransferFrom(address spender, address from, address to, uint256 value) - 76 : : public - 77 : : view - 78 : : virtual - 79 : : override(IERC7551Compliance) - 80 : : returns (bool) - 81 : : { - 82 : 14 : return _detectTransferRestrictionFrom(spender, from, to, value) - 83 : : == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); - 84 : : } - 85 : : - 86 : 198 : function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { - 87 : 198 : return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID - 88 : 197 : || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID - 89 : 196 : || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID - 90 : 108 : || interfaceId == type(IERC7551Compliance).interfaceId - 91 : 108 : || interfaceId == type(IERC3643IComplianceContract).interfaceId; - 92 : : } - 93 : : - 94 : : /*////////////////////////////////////////////////////////////// - 95 : : INTERNAL FUNCTIONS - 96 : : //////////////////////////////////////////////////////////////*/ - 97 : : - 98 : : /** - 99 : : * @notice Internal transfer restriction check. - 100 : : * @param from the origin address - 101 : : * @param to the destination address - 102 : : * @param value amount to transfer - 103 : : * @return restrictionCode The restriction code for this rule. - 104 : : */ - 105 : 0 : function _detectTransferRestriction(address from, address to, uint256 value) - 106 : : internal - 107 : : view - 108 : : virtual - 109 : : returns (uint8 restrictionCode); - 110 : : - 111 : : /** - 112 : : * @notice Internal transfer restriction check for spender-initiated transfers. - 113 : : * @param spender the caller executing the transfer - 114 : : * @param from the origin address - 115 : : * @param to the destination address - 116 : : * @param value amount to transfer - 117 : : * @return restrictionCode The restriction code for this rule. - 118 : : */ - 119 : 0 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) - 120 : : internal - 121 : : view - 122 : : virtual - 123 : : returns (uint8 restrictionCode); - 124 : : } + 18 : : /** + 19 : : * @title RuleTransferValidation — base transfer-restriction checks and interface support for rules. + 20 : : * @notice Exposes ERC-1404 / ERC-3643 / ERC-7551 read views delegating to internal restriction hooks. + 21 : : */ + 22 : : abstract contract RuleTransferValidation is + 23 : : VersionModule, + 24 : : IERC1404Extend, + 25 : : IERC3643ComplianceRead, + 26 : : IERC7551Compliance, + 27 : : IRule + 28 : : { + 29 : : /*////////////////////////////////////////////////////////////// + 30 : : PUBLIC FUNCTIONS + 31 : : //////////////////////////////////////////////////////////////*/ + 32 : : + 33 : : /** + 34 : : * @inheritdoc IERC1404 + 35 : : */ + 36 : 923 : function detectTransferRestriction(address from, address to, uint256 value) + 37 : : public + 38 : : view + 39 : : virtual + 40 : : override(IERC1404) + 41 : : returns (uint8) + 42 : : { + 43 : 923 : return _detectTransferRestriction(from, to, value); + 44 : : } + 45 : : + 46 : : /** + 47 : : * @inheritdoc IERC1404Extend + 48 : : */ + 49 : 58 : function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + 50 : : public + 51 : : view + 52 : : virtual + 53 : : override(IERC1404Extend) + 54 : : returns (uint8) + 55 : : { + 56 : 58 : return _detectTransferRestrictionFrom(spender, from, to, value); + 57 : : } + 58 : : + 59 : : /** + 60 : : * @notice Validate a transfer + 61 : : * @param from the origin address + 62 : : * @param to the destination address + 63 : : * @param amount to transfer + 64 : : * @return isValid => true if the transfer is valid, false otherwise + 65 : : * + 66 : : */ + 67 : 37 : function canTransfer(address from, address to, uint256 amount) + 68 : : public + 69 : : view + 70 : : override(IERC3643ComplianceRead) + 71 : : returns (bool isValid) + 72 : : { + 73 : 37 : return _detectTransferRestriction(from, to, amount) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 74 : : } + 75 : : + 76 : : /** + 77 : : * @inheritdoc IERC7551Compliance + 78 : : */ + 79 : 30 : function canTransferFrom(address spender, address from, address to, uint256 value) + 80 : : public + 81 : : view + 82 : : virtual + 83 : : override(IERC7551Compliance) + 84 : : returns (bool) + 85 : : { + 86 : 30 : return _detectTransferRestrictionFrom(spender, from, to, value) + 87 : : == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + 88 : : } + 89 : : + 90 : : /** + 91 : : * @notice Returns whether this contract implements the given interface. + 92 : : * @param interfaceId The ERC-165 interface identifier to query. + 93 : : * @return True if the interface is supported. + 94 : : */ + 95 : 230 : function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + 96 : 230 : return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + 97 : 229 : || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + 98 : 228 : || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId + 99 : 133 : || interfaceId == type(IERC3643IComplianceContract).interfaceId; + 100 : : } + 101 : : + 102 : : /*////////////////////////////////////////////////////////////// + 103 : : INTERNAL FUNCTIONS + 104 : : //////////////////////////////////////////////////////////////*/ + 105 : : + 106 : : /** + 107 : : * @notice Internal transfer restriction check. + 108 : : * @param from the origin address + 109 : : * @param to the destination address + 110 : : * @param value amount to transfer + 111 : : * @return restrictionCode The restriction code for this rule. + 112 : : */ + 113 : 0 : function _detectTransferRestriction(address from, address to, uint256 value) + 114 : : internal + 115 : : view + 116 : : virtual + 117 : : returns (uint8 restrictionCode); + 118 : : + 119 : : /** + 120 : : * @notice Internal transfer restriction check for spender-initiated transfers. + 121 : : * @param spender the caller executing the transfer + 122 : : * @param from the origin address + 123 : : * @param to the destination address + 124 : : * @param value amount to transfer + 125 : : * @return restrictionCode The restriction code for this rule. + 126 : : */ + 127 : 0 : function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + 128 : : internal + 129 : : view + 130 : : virtual + 131 : : returns (uint8 restrictionCode); + 132 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleWhitelistShared.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleWhitelistShared.sol.func-sort-c.html index a63e0a3..8719eec 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleWhitelistShared.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleWhitelistShared.sol.func-sort-c.html @@ -31,26 +31,26 @@ lcov.info Lines: - 22 - 22 - 100.0 % + 46 + 47 + 97.9 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 6 - 6 - 100.0 % + 11 + 12 + 91.7 % Branches: - 10 - 10 + 16 + 16 100.0 % @@ -69,28 +69,52 @@ Hit count Sort by hit count - RuleWhitelistShared.transferred.1 - 4 + RuleWhitelistShared._authorizeMintBurnManager + 0 + + + RuleWhitelistShared.setAllowBurn + 7 - RuleWhitelistShared.canReturnTransferRestrictionCode + RuleWhitelistShared.canReturnTransferRestrictionCode 10 - RuleWhitelistShared._transferredFrom - 11 + RuleWhitelistShared.onlyMintBurnManager + 13 - RuleWhitelistShared.messageForTransferRestriction - 14 + RuleWhitelistShared.setAllowMint + 13 - RuleWhitelistShared.transferred.0 + RuleWhitelistShared.transferred.1 + 13 + + + RuleWhitelistShared.messageForTransferRestriction 19 - RuleWhitelistShared._transferred - 28 + RuleWhitelistShared.transferred.0 + 19 + + + RuleWhitelistShared._transferredFrom + 32 + + + RuleWhitelistShared._transferred + 40 + + + RuleWhitelistShared._detectMintBurnRestriction + 166 + + + RuleWhitelistShared._setAllowMintBurn + 245
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleWhitelistShared.sol.func.html b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleWhitelistShared.sol.func.html index 10d6f20..bdcb47f 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleWhitelistShared.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleWhitelistShared.sol.func.html @@ -31,26 +31,26 @@ lcov.info Lines: - 22 - 22 - 100.0 % + 46 + 47 + 97.9 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 6 - 6 - 100.0 % + 11 + 12 + 91.7 % Branches: - 10 - 10 + 16 + 16 100.0 % @@ -69,28 +69,52 @@ Hit count Sort by hit count - RuleWhitelistShared._transferred - 28 + RuleWhitelistShared._authorizeMintBurnManager + 0 + + + RuleWhitelistShared._detectMintBurnRestriction + 166 + + + RuleWhitelistShared._setAllowMintBurn + 245 + + + RuleWhitelistShared._transferred + 40 - RuleWhitelistShared._transferredFrom - 11 + RuleWhitelistShared._transferredFrom + 32 - RuleWhitelistShared.canReturnTransferRestrictionCode + RuleWhitelistShared.canReturnTransferRestrictionCode 10 - RuleWhitelistShared.messageForTransferRestriction - 14 + RuleWhitelistShared.messageForTransferRestriction + 19 + + + RuleWhitelistShared.onlyMintBurnManager + 13 + + + RuleWhitelistShared.setAllowBurn + 7 + + + RuleWhitelistShared.setAllowMint + 13 - RuleWhitelistShared.transferred.0 + RuleWhitelistShared.transferred.0 19 - RuleWhitelistShared.transferred.1 - 4 + RuleWhitelistShared.transferred.1 + 13
diff --git a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleWhitelistShared.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleWhitelistShared.sol.gcov.html index e186485..23a6117 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/core/RuleWhitelistShared.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/core/RuleWhitelistShared.sol.gcov.html @@ -31,26 +31,26 @@ lcov.info Lines: - 22 - 22 - 100.0 % + 46 + 47 + 97.9 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 6 - 6 - 100.0 % + 11 + 12 + 91.7 % Branches: - 10 - 10 + 16 + 16 100.0 % @@ -93,101 +93,190 @@ 22 : : */ 23 : : bool public checkSpender; 24 : : - 25 : : /*////////////////////////////////////////////////////////////// - 26 : : EXTERNAL FUNCTIONS - 27 : : //////////////////////////////////////////////////////////////*/ - 28 : : - 29 : : /** - 30 : : * @notice Checks whether a restriction code is recognized by this rule. - 31 : : * @dev - 32 : : * Used to verify if a returned restriction code belongs to the whitelist rule. - 33 : : * @param restrictionCode The restriction code to validate. - 34 : : * @return isKnown True if the restriction code is recognized by this rule, false otherwise. - 35 : : */ - 36 : 10 : function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool isKnown) { - 37 : 10 : return restrictionCode == CODE_ADDRESS_FROM_NOT_WHITELISTED - 38 : 5 : || restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED - 39 : 2 : || restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED; - 40 : : } + 25 : : /** + 26 : : * @notice Whether this rule permits minting (`from == address(0)`). + 27 : : * @dev Mint/burn permission is an EXPLICIT flag, never list membership of `address(0)`. + 28 : : * The zero address is the ERC-20 mint/burn sentinel, not a participant: listing it would + 29 : : * make `isVerified(address(0))` return `true`, contradicting ERC-3643 (which defines + 30 : : * `isVerified` as "is this wallet a valid investor holding the required claims"). + 31 : : * Note this flag only gates the *operation*: a permitted mint still requires the RECIPIENT + 32 : : * to be whitelisted, so `allowMint = true` is not a bypass. + 33 : : */ + 34 : : bool public allowMint; + 35 : : + 36 : : /** + 37 : : * @notice Whether this rule permits burning (`to == address(0)`). + 38 : : * @dev See {allowMint}. A permitted burn still requires the SENDER to be whitelisted. + 39 : : */ + 40 : : bool public allowBurn; 41 : : - 42 : : /** - 43 : : * @notice Returns the human-readable message corresponding to a restriction code. - 44 : : * @dev - 45 : : * Returns a descriptive text that explains why a transfer was restricted. - 46 : : * @param restrictionCode The restriction code to decode. - 47 : : * @return message A human-readable explanation of the restriction. - 48 : : */ - 49 : 14 : function messageForTransferRestriction(uint8 restrictionCode) - 50 : : external - 51 : : pure - 52 : : override - 53 : : returns (string memory message) - 54 : : { - 55 [ + + ]: 14 : if (restrictionCode == CODE_ADDRESS_FROM_NOT_WHITELISTED) { - 56 : 6 : return TEXT_ADDRESS_FROM_NOT_WHITELISTED; - 57 [ + + ]: 8 : } else if (restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED) { - 58 : 4 : return TEXT_ADDRESS_TO_NOT_WHITELISTED; - 59 [ + + ]: 4 : } else if (restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED) { - 60 : 2 : return TEXT_ADDRESS_SPENDER_NOT_WHITELISTED; - 61 : : } else { - 62 : 2 : return TEXT_CODE_NOT_FOUND; - 63 : : } - 64 : : } - 65 : : - 66 : : /*////////////////////////////////////////////////////////////// - 67 : : PUBLIC FUNCTIONS - 68 : : //////////////////////////////////////////////////////////////*/ - 69 : : - 70 : : /** - 71 : : * @notice ERC-3643 hook called when a transfer occurs. - 72 : : * @dev - 73 : : * - Validates that both `from` and `to` addresses are whitelisted. - 74 : : * - Reverts if any restriction code other than `TRANSFER_OK` is returned. - 75 : : * - Validation only; does not modify state. - 76 : : * - Should be called during token transfer logic to enforce whitelist compliance. - 77 : : * @param from The address sending tokens. - 78 : : * @param to The address receiving tokens. - 79 : : * @param value The token amount being transferred. - 80 : : */ - 81 : 19 : function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { - 82 : 19 : _transferred(from, to, value); - 83 : : } - 84 : : - 85 : : /** - 86 : : * @notice hook called when a delegated transfer occurs (`transferFrom`). - 87 : : * @dev - 88 : : * - Validates that `spender`, `from`, and `to` are all whitelisted. - 89 : : * - Reverts if any restriction code other than `TRANSFER_OK` is returned. - 90 : : * - Validation only; does not modify state. - 91 : : * @param spender The address performing the transfer on behalf of another. - 92 : : * @param from The address from which tokens are transferred. - 93 : : * @param to The recipient address. - 94 : : * @param value The token amount being transferred. - 95 : : */ - 96 : 4 : function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { - 97 : 4 : _transferredFrom(spender, from, to, value); - 98 : : } - 99 : : - 100 : : /*////////////////////////////////////////////////////////////// - 101 : : INTERNAL FUNCTIONS - 102 : : //////////////////////////////////////////////////////////////*/ - 103 : : - 104 : 28 : function _transferred(address from, address to, uint256 value) internal view virtual override { - 105 : 28 : uint8 code = _detectTransferRestriction(from, to, value); - 106 [ + + ]: 28 : require( - 107 : : code == uint8(REJECTED_CODE_BASE.TRANSFER_OK), - 108 : : RuleWhitelist_InvalidTransfer(address(this), from, to, value, code) - 109 : : ); - 110 : : } - 111 : : - 112 : 11 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { - 113 : 11 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); - 114 [ + + ]: 11 : require( - 115 : : code == uint8(REJECTED_CODE_BASE.TRANSFER_OK), - 116 : : RuleWhitelist_InvalidTransferFrom(address(this), spender, from, to, value, code) - 117 : : ); - 118 : : } - 119 : : } + 42 : : /*////////////////////////////////////////////////////////////// + 43 : : ACCESS CONTROL + 44 : : //////////////////////////////////////////////////////////////*/ + 45 : : + 46 : 13 : modifier onlyMintBurnManager() { + 47 : 13 : _authorizeMintBurnManager(); + 48 : : _; + 49 : : } + 50 : : + 51 : : /*////////////////////////////////////////////////////////////// + 52 : : EXTERNAL FUNCTIONS + 53 : : //////////////////////////////////////////////////////////////*/ + 54 : : + 55 : : /** + 56 : : * @notice Checks whether a restriction code is recognized by this rule. + 57 : : * @dev + 58 : : * Used to verify if a returned restriction code belongs to the whitelist rule. + 59 : : * @param restrictionCode The restriction code to validate. + 60 : : * @return isKnown True if the restriction code is recognized by this rule, false otherwise. + 61 : : */ + 62 : 10 : function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool isKnown) { + 63 : 10 : return restrictionCode == CODE_ADDRESS_FROM_NOT_WHITELISTED + 64 : 5 : || restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED + 65 : 2 : || restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED || restrictionCode == CODE_MINT_NOT_ALLOWED + 66 : 2 : || restrictionCode == CODE_BURN_NOT_ALLOWED; + 67 : : } + 68 : : + 69 : : /** + 70 : : * @notice Enables or disables minting through this rule. + 71 : : * @param value The new value of the `allowMint` flag. + 72 : : */ + 73 : 13 : function setAllowMint(bool value) public virtual onlyMintBurnManager { + 74 : 10 : allowMint = value; + 75 : 10 : emit AllowMintUpdated(value); + 76 : : } + 77 : : + 78 : : /** + 79 : : * @notice Enables or disables burning through this rule. + 80 : : * @param value The new value of the `allowBurn` flag. + 81 : : */ + 82 : 7 : function setAllowBurn(bool value) public virtual onlyMintBurnManager { + 83 : 5 : allowBurn = value; + 84 : 5 : emit AllowBurnUpdated(value); + 85 : : } + 86 : : + 87 : : /** + 88 : : * @notice Returns the human-readable message corresponding to a restriction code. + 89 : : * @dev + 90 : : * Returns a descriptive text that explains why a transfer was restricted. + 91 : : * @param restrictionCode The restriction code to decode. + 92 : : * @return message A human-readable explanation of the restriction. + 93 : : */ + 94 : 19 : function messageForTransferRestriction(uint8 restrictionCode) + 95 : : external + 96 : : pure + 97 : : override + 98 : : returns (string memory message) + 99 : : { + 100 [ + + ]: 19 : if (restrictionCode == CODE_ADDRESS_FROM_NOT_WHITELISTED) { + 101 : 6 : return TEXT_ADDRESS_FROM_NOT_WHITELISTED; + 102 [ + + ]: 13 : } else if (restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED) { + 103 : 4 : return TEXT_ADDRESS_TO_NOT_WHITELISTED; + 104 [ + + ]: 9 : } else if (restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED) { + 105 : 2 : return TEXT_ADDRESS_SPENDER_NOT_WHITELISTED; + 106 [ + + ]: 7 : } else if (restrictionCode == CODE_MINT_NOT_ALLOWED) { + 107 : 3 : return TEXT_MINT_NOT_ALLOWED; + 108 [ + + ]: 4 : } else if (restrictionCode == CODE_BURN_NOT_ALLOWED) { + 109 : 2 : return TEXT_BURN_NOT_ALLOWED; + 110 : : } else { + 111 : 2 : return TEXT_CODE_NOT_FOUND; + 112 : : } + 113 : : } + 114 : : + 115 : : /*////////////////////////////////////////////////////////////// + 116 : : PUBLIC FUNCTIONS + 117 : : //////////////////////////////////////////////////////////////*/ + 118 : : + 119 : : /** + 120 : : * @notice ERC-3643 hook called when a transfer occurs. + 121 : : * @dev + 122 : : * - Validates that both `from` and `to` addresses are whitelisted. + 123 : : * - Reverts if any restriction code other than `TRANSFER_OK` is returned. + 124 : : * - Validation only; does not modify state. + 125 : : * - Should be called during token transfer logic to enforce whitelist compliance. + 126 : : * @param from The address sending tokens. + 127 : : * @param to The address receiving tokens. + 128 : : * @param value The token amount being transferred. + 129 : : */ + 130 : 19 : function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { + 131 : 19 : _transferred(from, to, value); + 132 : : } + 133 : : + 134 : : /** + 135 : : * @notice hook called when a delegated transfer occurs (`transferFrom`). + 136 : : * @dev + 137 : : * - Validates that `spender`, `from`, and `to` are all whitelisted. + 138 : : * - Reverts if any restriction code other than `TRANSFER_OK` is returned. + 139 : : * - Validation only; does not modify state. + 140 : : * @param spender The address performing the transfer on behalf of another. + 141 : : * @param from The address from which tokens are transferred. + 142 : : * @param to The recipient address. + 143 : : * @param value The token amount being transferred. + 144 : : */ + 145 : 13 : function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { + 146 : 13 : _transferredFrom(spender, from, to, value); + 147 : : } + 148 : : + 149 : : /*////////////////////////////////////////////////////////////// + 150 : : INTERNAL FUNCTIONS + 151 : : //////////////////////////////////////////////////////////////*/ + 152 : : + 153 : : /** + 154 : : * @notice Sets both mint/burn flags at once (deployment helper). + 155 : : * @param allowMint_ Whether minting is permitted. + 156 : : * @param allowBurn_ Whether burning is permitted. + 157 : : */ + 158 : 245 : function _setAllowMintBurn(bool allowMint_, bool allowBurn_) internal virtual { + 159 : 245 : allowMint = allowMint_; + 160 : 245 : allowBurn = allowBurn_; + 161 : 245 : emit AllowMintUpdated(allowMint_); + 162 : 245 : emit AllowBurnUpdated(allowBurn_); + 163 : : } + 164 : : + 165 : : /** + 166 : : * @notice Gates the mint/burn OPERATION, before any address is screened. + 167 : : * @dev Shared by {RuleWhitelistBase} and {RuleWhitelistWrapperBase} so the two can never drift. + 168 : : * @param from The sender (zero address for a mint). + 169 : : * @param to The recipient (zero address for a burn). + 170 : : * @return The restriction code, or TRANSFER_OK when the operation is permitted. + 171 : : */ + 172 : 166 : function _detectMintBurnRestriction(address from, address to) internal view virtual returns (uint8) { + 173 [ + ]: 166 : if (from == address(0) && !allowMint) { + 174 : 8 : return CODE_MINT_NOT_ALLOWED; + 175 : : } + 176 [ + ]: 158 : if (to == address(0) && !allowBurn) { + 177 : 3 : return CODE_BURN_NOT_ALLOWED; + 178 : : } + 179 : 155 : return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + 180 : : } + 181 : : + 182 : : /** + 183 : : * @notice Authorizes the caller to toggle `allowMint` / `allowBurn`; reverts otherwise. + 184 : : */ + 185 : 0 : function _authorizeMintBurnManager() internal view virtual; + 186 : : + 187 : : /** + 188 : : * @inheritdoc RuleNFTAdapter + 189 : : */ + 190 : 40 : function _transferred(address from, address to, uint256 value) internal view virtual override { + 191 : 40 : uint8 code = _detectTransferRestriction(from, to, value); + 192 [ + + ]: 40 : require( + 193 : : code == uint8(REJECTED_CODE_BASE.TRANSFER_OK), + 194 : : RuleWhitelist_InvalidTransfer(address(this), from, to, value, code) + 195 : : ); + 196 : : } + 197 : : + 198 : : /** + 199 : : * @inheritdoc RuleNFTAdapter + 200 : : */ + 201 : 32 : function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { + 202 : 32 : uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); + 203 [ + + ]: 32 : require( + 204 : : code == uint8(REJECTED_CODE_BASE.TRANSFER_OK), + 205 : : RuleWhitelist_InvalidTransferFrom(address(this), spender, from, to, value, code) + 206 : : ); + 207 : : } + 208 : : } diff --git a/doc/coverage/coverage/src/rules/validation/abstract/core/index-sort-b.html b/doc/coverage/coverage/src/rules/validation/abstract/core/index-sort-b.html index 8b5a077..cecfe8e 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/core/index-sort-b.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/core/index-sort-b.html @@ -31,26 +31,26 @@ lcov.info Lines: - 56 - 60 - 93.3 % + 79 + 84 + 94.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 19 - 23 - 82.6 % + 24 + 29 + 82.8 % Branches: - 14 - 14 + 20 + 20 100.0 % @@ -84,10 +84,10 @@ RuleTransferValidation.sol -
87.5%87.5%
+
86.7%86.7%
- 87.5 % - 14 / 16 + 86.7 % + 13 / 15 71.4 % 5 / 7 - @@ -108,14 +108,14 @@ RuleWhitelistShared.sol -
100.0%
+
97.9%97.9%
+ 97.9 % + 46 / 47 + 91.7 % + 11 / 12 100.0 % - 22 / 22 - 100.0 % - 6 / 6 - 100.0 % - 10 / 10 + 16 / 16 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/core/index-sort-f.html b/doc/coverage/coverage/src/rules/validation/abstract/core/index-sort-f.html index 38fe74f..83d45b4 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/core/index-sort-f.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/core/index-sort-f.html @@ -31,26 +31,26 @@ lcov.info Lines: - 56 - 60 - 93.3 % + 79 + 84 + 94.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 19 - 23 - 82.6 % + 24 + 29 + 82.8 % Branches: - 14 - 14 + 20 + 20 100.0 % @@ -84,10 +84,10 @@ RuleTransferValidation.sol -
87.5%87.5%
+
86.7%86.7%
- 87.5 % - 14 / 16 + 86.7 % + 13 / 15 71.4 % 5 / 7 - @@ -108,14 +108,14 @@ RuleWhitelistShared.sol -
100.0%
+
97.9%97.9%
+ 97.9 % + 46 / 47 + 91.7 % + 11 / 12 100.0 % - 22 / 22 - 100.0 % - 6 / 6 - 100.0 % - 10 / 10 + 16 / 16 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/core/index-sort-l.html b/doc/coverage/coverage/src/rules/validation/abstract/core/index-sort-l.html index 809d81e..c1ef5f8 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/core/index-sort-l.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/core/index-sort-l.html @@ -31,26 +31,26 @@ lcov.info Lines: - 56 - 60 - 93.3 % + 79 + 84 + 94.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 19 - 23 - 82.6 % + 24 + 29 + 82.8 % Branches: - 14 - 14 + 20 + 20 100.0 % @@ -84,10 +84,10 @@ RuleTransferValidation.sol -
87.5%87.5%
+
86.7%86.7%
- 87.5 % - 14 / 16 + 86.7 % + 13 / 15 71.4 % 5 / 7 - @@ -108,14 +108,14 @@ RuleWhitelistShared.sol -
100.0%
+
97.9%97.9%
+ 97.9 % + 46 / 47 + 91.7 % + 11 / 12 100.0 % - 22 / 22 - 100.0 % - 6 / 6 - 100.0 % - 10 / 10 + 16 / 16 diff --git a/doc/coverage/coverage/src/rules/validation/abstract/core/index.html b/doc/coverage/coverage/src/rules/validation/abstract/core/index.html index e7fb9a0..e4067be 100644 --- a/doc/coverage/coverage/src/rules/validation/abstract/core/index.html +++ b/doc/coverage/coverage/src/rules/validation/abstract/core/index.html @@ -31,26 +31,26 @@ lcov.info Lines: - 56 - 60 - 93.3 % + 79 + 84 + 94.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 19 - 23 - 82.6 % + 24 + 29 + 82.8 % Branches: - 14 - 14 + 20 + 20 100.0 % @@ -96,10 +96,10 @@ RuleTransferValidation.sol -
87.5%87.5%
+
86.7%86.7%
- 87.5 % - 14 / 16 + 86.7 % + 13 / 15 71.4 % 5 / 7 - @@ -108,14 +108,14 @@ RuleWhitelistShared.sol -
100.0%
+
97.9%97.9%
+ 97.9 % + 46 / 47 + 91.7 % + 11 / 12 100.0 % - 22 / 22 - 100.0 % - 6 / 6 - 100.0 % - 10 / 10 + 16 / 16 diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklist.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklist.sol.func-sort-c.html index c9ba010..fecfc7d 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklist.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklist.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 6 @@ -69,28 +69,28 @@ Hit count Sort by hit count - RuleBlacklist._authorizeAddressListRemove + RuleBlacklist._authorizeAddressListRemove 1 - RuleBlacklist._msgData + RuleBlacklist._msgData 1 - RuleBlacklist._authorizeAddressListAdd - 28 + RuleBlacklist._authorizeAddressListAdd + 30 - RuleBlacklist._msgSender - 83 + RuleBlacklist._msgSender + 88 - RuleBlacklist._contextSuffixLength - 84 + RuleBlacklist._contextSuffixLength + 89 - RuleBlacklist.supportsInterface - 91 + RuleBlacklist.supportsInterface + 92
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklist.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklist.sol.func.html index a23d580..298b807 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklist.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklist.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 6 @@ -69,28 +69,28 @@ Hit count Sort by hit count - RuleBlacklist._authorizeAddressListAdd - 28 + RuleBlacklist._authorizeAddressListAdd + 30 - RuleBlacklist._authorizeAddressListRemove + RuleBlacklist._authorizeAddressListRemove 1 - RuleBlacklist._contextSuffixLength - 84 + RuleBlacklist._contextSuffixLength + 89 - RuleBlacklist._msgData + RuleBlacklist._msgData 1 - RuleBlacklist._msgSender - 83 + RuleBlacklist._msgSender + 88 - RuleBlacklist.supportsInterface - 91 + RuleBlacklist.supportsInterface + 92
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklist.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklist.sol.gcov.html index c42a129..c13246c 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklist.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklist.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 6 @@ -100,41 +100,64 @@ 29 : : PUBLIC FUNCTIONS 30 : : //////////////////////////////////////////////////////////////*/ 31 : : - 32 : 91 : function supportsInterface(bytes4 interfaceId) - 33 : : public - 34 : : view - 35 : : virtual - 36 : : override(AccessControlEnumerable, RuleBlacklistBase) - 37 : : returns (bool) - 38 : : { - 39 : 91 : return AccessControlEnumerable.supportsInterface(interfaceId) - 40 : 61 : || RuleBlacklistBase.supportsInterface(interfaceId); - 41 : : } - 42 : : - 43 : : /*////////////////////////////////////////////////////////////// - 44 : : ACCESS CONTROL - 45 : : //////////////////////////////////////////////////////////////*/ - 46 : : - 47 : 28 : function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} - 48 : : - 49 : 1 : function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} - 50 : : - 51 : : /*////////////////////////////////////////////////////////////// - 52 : : INTERNAL FUNCTIONS - 53 : : //////////////////////////////////////////////////////////////*/ - 54 : : - 55 : 83 : function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { - 56 : 83 : return super._msgSender(); - 57 : : } - 58 : : - 59 : 1 : function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { - 60 : 1 : return super._msgData(); - 61 : : } - 62 : : - 63 : 84 : function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { - 64 : 84 : return super._contextSuffixLength(); - 65 : : } - 66 : : } + 32 : : /** + 33 : : * @notice Indicates whether this contract supports a given interface. + 34 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 35 : : * @return True if the interface is supported. + 36 : : */ + 37 : 92 : function supportsInterface(bytes4 interfaceId) + 38 : : public + 39 : : view + 40 : : virtual + 41 : : override(AccessControlEnumerable, RuleBlacklistBase) + 42 : : returns (bool) + 43 : : { + 44 : 92 : return AccessControlEnumerable.supportsInterface(interfaceId) + 45 : 62 : || RuleBlacklistBase.supportsInterface(interfaceId); + 46 : : } + 47 : : + 48 : : /*////////////////////////////////////////////////////////////// + 49 : : ACCESS CONTROL + 50 : : //////////////////////////////////////////////////////////////*/ + 51 : : + 52 : : /** + 53 : : * @notice Restricts adding addresses to the blacklist to holders of ADDRESS_LIST_ADD_ROLE. + 54 : : */ + 55 : 30 : function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + 56 : : + 57 : : /** + 58 : : * @notice Restricts removing addresses from the blacklist to holders of ADDRESS_LIST_REMOVE_ROLE. + 59 : : */ + 60 : 1 : function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + 61 : : + 62 : : /*////////////////////////////////////////////////////////////// + 63 : : INTERNAL FUNCTIONS + 64 : : //////////////////////////////////////////////////////////////*/ + 65 : : + 66 : : /** + 67 : : * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + 68 : : * @return sender The address of the message sender. + 69 : : */ + 70 : 88 : function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { + 71 : 88 : return super._msgSender(); + 72 : : } + 73 : : + 74 : : /** + 75 : : * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + 76 : : * @return The message calldata. + 77 : : */ + 78 : 1 : function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { + 79 : 1 : return super._msgData(); + 80 : : } + 81 : : + 82 : : /** + 83 : : * @notice Returns the length of the context suffix appended by the forwarder. + 84 : : * @return The context suffix length in bytes. + 85 : : */ + 86 : 89 : function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { + 87 : 89 : return super._contextSuffixLength(); + 88 : : } + 89 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol.func-sort-c.html index b60d6f1..75b32fc 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 8 - 8 + 11 + 11 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 5 - 5 + 6 + 6 100.0 % @@ -69,23 +69,27 @@ Hit count Sort by hit count - RuleBlacklistOwnable2Step._msgData + RuleBlacklistOwnable2Step._msgData 1 - RuleBlacklistOwnable2Step._authorizeAddressListAdd + RuleBlacklistOwnable2Step._authorizeAddressListAdd 2 - RuleBlacklistOwnable2Step._authorizeAddressListRemove + RuleBlacklistOwnable2Step._authorizeAddressListRemove 2 - RuleBlacklistOwnable2Step._msgSender + RuleBlacklistOwnable2Step.supportsInterface + 6 + + + RuleBlacklistOwnable2Step._msgSender 12 - RuleBlacklistOwnable2Step._contextSuffixLength + RuleBlacklistOwnable2Step._contextSuffixLength 13 diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol.func.html index bc36b87..9af0941 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 8 - 8 + 11 + 11 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 5 - 5 + 6 + 6 100.0 % @@ -69,25 +69,29 @@ Hit count Sort by hit count - RuleBlacklistOwnable2Step._authorizeAddressListAdd + RuleBlacklistOwnable2Step._authorizeAddressListAdd 2 - RuleBlacklistOwnable2Step._authorizeAddressListRemove + RuleBlacklistOwnable2Step._authorizeAddressListRemove 2 - RuleBlacklistOwnable2Step._contextSuffixLength + RuleBlacklistOwnable2Step._contextSuffixLength 13 - RuleBlacklistOwnable2Step._msgData + RuleBlacklistOwnable2Step._msgData 1 - RuleBlacklistOwnable2Step._msgSender + RuleBlacklistOwnable2Step._msgSender 12 + + RuleBlacklistOwnable2Step.supportsInterface + 6 +
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol.gcov.html index c82933f..b7a1e4d 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 8 - 8 + 11 + 11 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 5 - 5 + 6 + 6 100.0 % @@ -75,44 +75,88 @@ 4 : : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 5 : : import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; 6 : : import {Context} from "@openzeppelin/contracts/utils/Context.sol"; - 7 : : import {RuleBlacklistBase} from "../abstract/base/RuleBlacklistBase.sol"; - 8 : : import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; - 9 : : - 10 : : /** - 11 : : * @title RuleBlacklistOwnable2Step - 12 : : * @notice Ownable2Step variant of RuleBlacklist with owner-based authorization hooks. - 13 : : */ - 14 : : contract RuleBlacklistOwnable2Step is RuleBlacklistBase, Ownable2Step { - 15 : : /*////////////////////////////////////////////////////////////// - 16 : : CONSTRUCTOR - 17 : : //////////////////////////////////////////////////////////////*/ - 18 : : - 19 : : constructor(address owner, address forwarderIrrevocable) RuleBlacklistBase(forwarderIrrevocable) Ownable(owner) {} - 20 : : - 21 : : /*////////////////////////////////////////////////////////////// - 22 : : ACCESS CONTROL - 23 : : //////////////////////////////////////////////////////////////*/ - 24 : : - 25 : 2 : function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + 7 : : import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; + 8 : : import {RuleBlacklistBase} from "../abstract/base/RuleBlacklistBase.sol"; + 9 : : import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; + 10 : : + 11 : : /** + 12 : : * @title RuleBlacklistOwnable2Step + 13 : : * @notice Ownable2Step variant of RuleBlacklist with owner-based authorization hooks. + 14 : : */ + 15 : : contract RuleBlacklistOwnable2Step is RuleBlacklistBase, Ownable2Step, Ownable2StepERC165Module { + 16 : : /*////////////////////////////////////////////////////////////// + 17 : : CONSTRUCTOR + 18 : : //////////////////////////////////////////////////////////////*/ + 19 : : + 20 : : /** + 21 : : * @notice Deploys the rule and sets the initial owner and meta-transaction forwarder. + 22 : : * @param owner Contract owner. + 23 : : * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. + 24 : : */ + 25 : : constructor(address owner, address forwarderIrrevocable) RuleBlacklistBase(forwarderIrrevocable) Ownable(owner) {} 26 : : - 27 : 2 : function _authorizeAddressListRemove() internal view virtual override onlyOwner {} - 28 : : - 29 : : /*////////////////////////////////////////////////////////////// - 30 : : INTERNAL FUNCTIONS - 31 : : //////////////////////////////////////////////////////////////*/ - 32 : : - 33 : 12 : function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { - 34 : 12 : return super._msgSender(); - 35 : : } - 36 : : - 37 : 1 : function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { - 38 : 1 : return super._msgData(); - 39 : : } - 40 : : - 41 : 13 : function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { - 42 : 13 : return super._contextSuffixLength(); - 43 : : } - 44 : : } + 27 : : /*////////////////////////////////////////////////////////////// + 28 : : PUBLIC FUNCTIONS + 29 : : //////////////////////////////////////////////////////////////*/ + 30 : : + 31 : : /** + 32 : : * @notice Indicates whether this contract supports a given interface. + 33 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 34 : : * @return True if the interface is supported. + 35 : : */ + 36 : 6 : function supportsInterface(bytes4 interfaceId) + 37 : : public + 38 : : view + 39 : : virtual + 40 : : override(RuleBlacklistBase, Ownable2StepERC165Module) + 41 : : returns (bool) + 42 : : { + 43 : 6 : return Ownable2StepERC165Module.supportsInterface(interfaceId) + 44 : 3 : || RuleBlacklistBase.supportsInterface(interfaceId); + 45 : : } + 46 : : + 47 : : /*////////////////////////////////////////////////////////////// + 48 : : ACCESS CONTROL + 49 : : //////////////////////////////////////////////////////////////*/ + 50 : : + 51 : : /** + 52 : : * @notice Restricts adding addresses to the blacklist to the contract owner. + 53 : : */ + 54 : 2 : function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + 55 : : + 56 : : /** + 57 : : * @notice Restricts removing addresses from the blacklist to the contract owner. + 58 : : */ + 59 : 2 : function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + 60 : : + 61 : : /*////////////////////////////////////////////////////////////// + 62 : : INTERNAL FUNCTIONS + 63 : : //////////////////////////////////////////////////////////////*/ + 64 : : + 65 : : /** + 66 : : * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + 67 : : * @return sender The address of the message sender. + 68 : : */ + 69 : 12 : function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { + 70 : 12 : return super._msgSender(); + 71 : : } + 72 : : + 73 : : /** + 74 : : * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + 75 : : * @return The message calldata. + 76 : : */ + 77 : 1 : function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { + 78 : 1 : return super._msgData(); + 79 : : } + 80 : : + 81 : : /** + 82 : : * @notice Returns the length of the context suffix appended by the forwarder. + 83 : : * @return The context suffix length in bytes. + 84 : : */ + 85 : 13 : function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { + 86 : 13 : return super._contextSuffixLength(); + 87 : : } + 88 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980.sol.func-sort-c.html index bb5cee0..055e2d8 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 12 - 12 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 8 - 8 + 9 + 9 100.0 % @@ -69,36 +69,40 @@ Hit count Sort by hit count - RuleERC2980._msgData + RuleERC2980._msgData 1 - RuleERC2980.supportsInterface + RuleERC2980.supportsInterface 1 - RuleERC2980._authorizeFrozenlistRemove + RuleERC2980._authorizeMintBurnManager + 5 + + + RuleERC2980._authorizeFrozenlistRemove 7 - RuleERC2980._authorizeWhitelistRemove + RuleERC2980._authorizeWhitelistRemove 8 - RuleERC2980._authorizeFrozenlistAdd - 22 + RuleERC2980._authorizeFrozenlistAdd + 24 - RuleERC2980._authorizeWhitelistAdd - 44 + RuleERC2980._authorizeWhitelistAdd + 48 - RuleERC2980._msgSender - 242 + RuleERC2980._msgSender + 259 - RuleERC2980._contextSuffixLength - 243 + RuleERC2980._contextSuffixLength + 260
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980.sol.func.html index fa45b3d..540a6a9 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 12 - 12 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 8 - 8 + 9 + 9 100.0 % @@ -69,35 +69,39 @@ Hit count Sort by hit count - RuleERC2980._authorizeFrozenlistAdd - 22 + RuleERC2980._authorizeFrozenlistAdd + 24 - RuleERC2980._authorizeFrozenlistRemove + RuleERC2980._authorizeFrozenlistRemove 7 - RuleERC2980._authorizeWhitelistAdd - 44 + RuleERC2980._authorizeMintBurnManager + 5 - RuleERC2980._authorizeWhitelistRemove + RuleERC2980._authorizeWhitelistAdd + 48 + + + RuleERC2980._authorizeWhitelistRemove 8 - RuleERC2980._contextSuffixLength - 243 + RuleERC2980._contextSuffixLength + 260 - RuleERC2980._msgData + RuleERC2980._msgData 1 - RuleERC2980._msgSender - 242 + RuleERC2980._msgSender + 259 - RuleERC2980.supportsInterface + RuleERC2980.supportsInterface 1 diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980.sol.gcov.html index 682d2d2..5286658 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 12 - 12 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 8 - 8 + 9 + 9 100.0 % @@ -108,10 +108,10 @@ 37 : : /** 38 : : * @param admin Address that receives `DEFAULT_ADMIN_ROLE` (implicitly holds all roles). 39 : : * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. - 40 : : * @param allowBurn If true, whitelists `address(0)` at deployment to allow burn/redemption flows. + 40 : : * @param allowMintBurn When true, permits both minting and burning (sets `allowMint` and `allowBurn`). 41 : : */ - 42 : : constructor(address admin, address forwarderIrrevocable, bool allowBurn) - 43 : : RuleERC2980Base(forwarderIrrevocable, allowBurn) + 42 : : constructor(address admin, address forwarderIrrevocable, bool allowMintBurn) + 43 : : RuleERC2980Base(forwarderIrrevocable, allowMintBurn) 44 : : AccessControlModuleStandalone(admin) 45 : : {} 46 : : @@ -119,44 +119,78 @@ 48 : : PUBLIC FUNCTIONS 49 : : //////////////////////////////////////////////////////////////*/ 50 : : - 51 : 1 : function supportsInterface(bytes4 interfaceId) - 52 : : public - 53 : : view - 54 : : virtual - 55 : : override(AccessControlEnumerable, RuleERC2980Base) - 56 : : returns (bool) - 57 : : { - 58 : 1 : return AccessControlEnumerable.supportsInterface(interfaceId) || RuleERC2980Base.supportsInterface(interfaceId); - 59 : : } - 60 : : - 61 : : /*////////////////////////////////////////////////////////////// - 62 : : ACCESS CONTROL - 63 : : //////////////////////////////////////////////////////////////*/ - 64 : : - 65 : 44 : function _authorizeWhitelistAdd() internal view virtual override onlyRole(WHITELIST_ADD_ROLE) {} - 66 : : - 67 : 8 : function _authorizeWhitelistRemove() internal view virtual override onlyRole(WHITELIST_REMOVE_ROLE) {} - 68 : : - 69 : 22 : function _authorizeFrozenlistAdd() internal view virtual override onlyRole(FROZENLIST_ADD_ROLE) {} - 70 : : - 71 : 7 : function _authorizeFrozenlistRemove() internal view virtual override onlyRole(FROZENLIST_REMOVE_ROLE) {} - 72 : : - 73 : : /*////////////////////////////////////////////////////////////// - 74 : : INTERNAL FUNCTIONS - 75 : : //////////////////////////////////////////////////////////////*/ - 76 : : - 77 : 242 : function _msgSender() internal view virtual override(Context, RuleERC2980Base) returns (address sender) { - 78 : 242 : return super._msgSender(); - 79 : : } - 80 : : - 81 : 1 : function _msgData() internal view virtual override(Context, RuleERC2980Base) returns (bytes calldata) { - 82 : 1 : return super._msgData(); - 83 : : } + 51 : : /** + 52 : : * @notice Indicates whether this contract supports a given interface. + 53 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 54 : : * @return True if the interface is supported. + 55 : : */ + 56 : 1 : function supportsInterface(bytes4 interfaceId) + 57 : : public + 58 : : view + 59 : : virtual + 60 : : override(AccessControlEnumerable, RuleERC2980Base) + 61 : : returns (bool) + 62 : : { + 63 : 1 : return AccessControlEnumerable.supportsInterface(interfaceId) || RuleERC2980Base.supportsInterface(interfaceId); + 64 : : } + 65 : : + 66 : : /*////////////////////////////////////////////////////////////// + 67 : : ACCESS CONTROL + 68 : : //////////////////////////////////////////////////////////////*/ + 69 : : + 70 : : /** + 71 : : * @notice Restricts adding addresses to the whitelist to holders of WHITELIST_ADD_ROLE. + 72 : : */ + 73 : : /** + 74 : : * @notice Restricts toggling `allowMint` / `allowBurn` to holders of DEFAULT_ADMIN_ROLE. + 75 : : */ + 76 : 5 : function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + 77 : : + 78 : 48 : function _authorizeWhitelistAdd() internal view virtual override onlyRole(WHITELIST_ADD_ROLE) {} + 79 : : + 80 : : /** + 81 : : * @notice Restricts removing addresses from the whitelist to holders of WHITELIST_REMOVE_ROLE. + 82 : : */ + 83 : 8 : function _authorizeWhitelistRemove() internal view virtual override onlyRole(WHITELIST_REMOVE_ROLE) {} 84 : : - 85 : 243 : function _contextSuffixLength() internal view virtual override(Context, RuleERC2980Base) returns (uint256) { - 86 : 243 : return super._contextSuffixLength(); - 87 : : } - 88 : : } + 85 : : /** + 86 : : * @notice Restricts adding addresses to the frozenlist to holders of FROZENLIST_ADD_ROLE. + 87 : : */ + 88 : 24 : function _authorizeFrozenlistAdd() internal view virtual override onlyRole(FROZENLIST_ADD_ROLE) {} + 89 : : + 90 : : /** + 91 : : * @notice Restricts removing addresses from the frozenlist to holders of FROZENLIST_REMOVE_ROLE. + 92 : : */ + 93 : 7 : function _authorizeFrozenlistRemove() internal view virtual override onlyRole(FROZENLIST_REMOVE_ROLE) {} + 94 : : + 95 : : /*////////////////////////////////////////////////////////////// + 96 : : INTERNAL FUNCTIONS + 97 : : //////////////////////////////////////////////////////////////*/ + 98 : : + 99 : : /** + 100 : : * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + 101 : : * @return sender The address of the message sender. + 102 : : */ + 103 : 259 : function _msgSender() internal view virtual override(Context, RuleERC2980Base) returns (address sender) { + 104 : 259 : return super._msgSender(); + 105 : : } + 106 : : + 107 : : /** + 108 : : * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + 109 : : * @return The message calldata. + 110 : : */ + 111 : 1 : function _msgData() internal view virtual override(Context, RuleERC2980Base) returns (bytes calldata) { + 112 : 1 : return super._msgData(); + 113 : : } + 114 : : + 115 : : /** + 116 : : * @notice Returns the length of the context suffix appended by the forwarder. + 117 : : * @return The context suffix length in bytes. + 118 : : */ + 119 : 260 : function _contextSuffixLength() internal view virtual override(Context, RuleERC2980Base) returns (uint256) { + 120 : 260 : return super._contextSuffixLength(); + 121 : : } + 122 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol.func-sort-c.html index 5d34555..3a8b6d3 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 10 - 10 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 7 - 7 + 9 + 9 100.0 % @@ -69,32 +69,40 @@ Hit count Sort by hit count - RuleERC2980Ownable2Step._msgData + RuleERC2980Ownable2Step._msgData 1 - RuleERC2980Ownable2Step._authorizeFrozenlistRemove + RuleERC2980Ownable2Step._authorizeFrozenlistRemove 2 - RuleERC2980Ownable2Step._authorizeWhitelistRemove + RuleERC2980Ownable2Step._authorizeMintBurnManager 3 - RuleERC2980Ownable2Step._authorizeFrozenlistAdd + RuleERC2980Ownable2Step._authorizeWhitelistRemove + 3 + + + RuleERC2980Ownable2Step.supportsInterface + 5 + + + RuleERC2980Ownable2Step._authorizeFrozenlistAdd 6 - RuleERC2980Ownable2Step._authorizeWhitelistAdd + RuleERC2980Ownable2Step._authorizeWhitelistAdd 7 - RuleERC2980Ownable2Step._msgSender - 30 + RuleERC2980Ownable2Step._msgSender + 34 - RuleERC2980Ownable2Step._contextSuffixLength - 31 + RuleERC2980Ownable2Step._contextSuffixLength + 35
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol.func.html index 815c45e..4ec630f 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 10 - 10 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 7 - 7 + 9 + 9 100.0 % @@ -69,32 +69,40 @@ Hit count Sort by hit count - RuleERC2980Ownable2Step._authorizeFrozenlistAdd + RuleERC2980Ownable2Step._authorizeFrozenlistAdd 6 - RuleERC2980Ownable2Step._authorizeFrozenlistRemove + RuleERC2980Ownable2Step._authorizeFrozenlistRemove 2 - RuleERC2980Ownable2Step._authorizeWhitelistAdd + RuleERC2980Ownable2Step._authorizeMintBurnManager + 3 + + + RuleERC2980Ownable2Step._authorizeWhitelistAdd 7 - RuleERC2980Ownable2Step._authorizeWhitelistRemove + RuleERC2980Ownable2Step._authorizeWhitelistRemove 3 - RuleERC2980Ownable2Step._contextSuffixLength - 31 + RuleERC2980Ownable2Step._contextSuffixLength + 35 - RuleERC2980Ownable2Step._msgData + RuleERC2980Ownable2Step._msgData 1 - RuleERC2980Ownable2Step._msgSender - 30 + RuleERC2980Ownable2Step._msgSender + 34 + + + RuleERC2980Ownable2Step.supportsInterface + 5
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol.gcov.html index a03e0c3..e4524c2 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 10 - 10 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 7 - 7 + 9 + 9 100.0 % @@ -75,56 +75,105 @@ 4 : : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 5 : : import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; 6 : : import {Context} from "@openzeppelin/contracts/utils/Context.sol"; - 7 : : import {RuleERC2980Base} from "../abstract/base/RuleERC2980Base.sol"; - 8 : : - 9 : : /** - 10 : : * @title RuleERC2980Ownable2Step - 11 : : * @notice Ownable2Step variant of RuleERC2980 with owner-based authorization hooks. - 12 : : * @dev All whitelist and frozenlist management functions are restricted to the contract owner. - 13 : : */ - 14 : : contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step { - 15 : : /*////////////////////////////////////////////////////////////// - 16 : : CONSTRUCTOR - 17 : : //////////////////////////////////////////////////////////////*/ - 18 : : - 19 : : /** - 20 : : * @param owner Contract owner. - 21 : : * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. - 22 : : * @param allowBurn If true, whitelists `address(0)` at deployment to allow burn/redemption flows. - 23 : : */ - 24 : : constructor(address owner, address forwarderIrrevocable, bool allowBurn) - 25 : : RuleERC2980Base(forwarderIrrevocable, allowBurn) - 26 : : Ownable(owner) - 27 : : {} - 28 : : - 29 : : /*////////////////////////////////////////////////////////////// - 30 : : ACCESS CONTROL - 31 : : //////////////////////////////////////////////////////////////*/ - 32 : : - 33 : 7 : function _authorizeWhitelistAdd() internal view virtual override onlyOwner {} - 34 : : - 35 : 3 : function _authorizeWhitelistRemove() internal view virtual override onlyOwner {} - 36 : : - 37 : 6 : function _authorizeFrozenlistAdd() internal view virtual override onlyOwner {} - 38 : : - 39 : 2 : function _authorizeFrozenlistRemove() internal view virtual override onlyOwner {} - 40 : : - 41 : : /*////////////////////////////////////////////////////////////// - 42 : : INTERNAL FUNCTIONS - 43 : : //////////////////////////////////////////////////////////////*/ - 44 : : - 45 : 30 : function _msgSender() internal view virtual override(Context, RuleERC2980Base) returns (address sender) { - 46 : 30 : return super._msgSender(); + 7 : : import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; + 8 : : import {RuleERC2980Base} from "../abstract/base/RuleERC2980Base.sol"; + 9 : : + 10 : : /** + 11 : : * @title RuleERC2980Ownable2Step + 12 : : * @notice Ownable2Step variant of RuleERC2980 with owner-based authorization hooks. + 13 : : * @dev All whitelist and frozenlist management functions are restricted to the contract owner. + 14 : : */ + 15 : : contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step, Ownable2StepERC165Module { + 16 : : /*////////////////////////////////////////////////////////////// + 17 : : CONSTRUCTOR + 18 : : //////////////////////////////////////////////////////////////*/ + 19 : : + 20 : : /** + 21 : : * @param owner Contract owner. + 22 : : * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. + 23 : : * @param allowMintBurn When true, permits both minting and burning (sets `allowMint` and `allowBurn`). + 24 : : */ + 25 : : constructor(address owner, address forwarderIrrevocable, bool allowMintBurn) + 26 : : RuleERC2980Base(forwarderIrrevocable, allowMintBurn) + 27 : : Ownable(owner) + 28 : : {} + 29 : : + 30 : : /*////////////////////////////////////////////////////////////// + 31 : : PUBLIC FUNCTIONS + 32 : : //////////////////////////////////////////////////////////////*/ + 33 : : + 34 : : /** + 35 : : * @notice Indicates whether this contract supports a given interface. + 36 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 37 : : * @return True if the interface is supported. + 38 : : */ + 39 : 5 : function supportsInterface(bytes4 interfaceId) + 40 : : public + 41 : : view + 42 : : virtual + 43 : : override(RuleERC2980Base, Ownable2StepERC165Module) + 44 : : returns (bool) + 45 : : { + 46 : 5 : return Ownable2StepERC165Module.supportsInterface(interfaceId) || RuleERC2980Base.supportsInterface(interfaceId); 47 : : } 48 : : - 49 : 1 : function _msgData() internal view virtual override(Context, RuleERC2980Base) returns (bytes calldata) { - 50 : 1 : return super._msgData(); - 51 : : } + 49 : : /*////////////////////////////////////////////////////////////// + 50 : : ACCESS CONTROL + 51 : : //////////////////////////////////////////////////////////////*/ 52 : : - 53 : 31 : function _contextSuffixLength() internal view virtual override(Context, RuleERC2980Base) returns (uint256) { - 54 : 31 : return super._contextSuffixLength(); - 55 : : } - 56 : : } + 53 : : /** + 54 : : * @notice Restricts adding addresses to the whitelist to the contract owner. + 55 : : */ + 56 : : /** + 57 : : * @notice Restricts toggling `allowMint` / `allowBurn` to the contract owner. + 58 : : */ + 59 : 3 : function _authorizeMintBurnManager() internal view virtual override onlyOwner {} + 60 : : + 61 : 7 : function _authorizeWhitelistAdd() internal view virtual override onlyOwner {} + 62 : : + 63 : : /** + 64 : : * @notice Restricts removing addresses from the whitelist to the contract owner. + 65 : : */ + 66 : 3 : function _authorizeWhitelistRemove() internal view virtual override onlyOwner {} + 67 : : + 68 : : /** + 69 : : * @notice Restricts adding addresses to the frozenlist to the contract owner. + 70 : : */ + 71 : 6 : function _authorizeFrozenlistAdd() internal view virtual override onlyOwner {} + 72 : : + 73 : : /** + 74 : : * @notice Restricts removing addresses from the frozenlist to the contract owner. + 75 : : */ + 76 : 2 : function _authorizeFrozenlistRemove() internal view virtual override onlyOwner {} + 77 : : + 78 : : /*////////////////////////////////////////////////////////////// + 79 : : INTERNAL FUNCTIONS + 80 : : //////////////////////////////////////////////////////////////*/ + 81 : : + 82 : : /** + 83 : : * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + 84 : : * @return sender The address of the message sender. + 85 : : */ + 86 : 34 : function _msgSender() internal view virtual override(Context, RuleERC2980Base) returns (address sender) { + 87 : 34 : return super._msgSender(); + 88 : : } + 89 : : + 90 : : /** + 91 : : * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + 92 : : * @return The message calldata. + 93 : : */ + 94 : 1 : function _msgData() internal view virtual override(Context, RuleERC2980Base) returns (bytes calldata) { + 95 : 1 : return super._msgData(); + 96 : : } + 97 : : + 98 : : /** + 99 : : * @notice Returns the length of the context suffix appended by the forwarder. + 100 : : * @return The context suffix length in bytes. + 101 : : */ + 102 : 35 : function _contextSuffixLength() internal view virtual override(Context, RuleERC2980Base) returns (uint256) { + 103 : 35 : return super._contextSuffixLength(); + 104 : : } + 105 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistry.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistry.sol.func-sort-c.html index e38abb4..36898ac 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistry.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistry.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 2 @@ -69,12 +69,12 @@ Hit count Sort by hit count - RuleIdentityRegistry._authorizeIdentityRegistryManager - 5 + RuleIdentityRegistry._authorizeIdentityRegistryManager + 12 - RuleIdentityRegistry.supportsInterface - 15 + RuleIdentityRegistry.supportsInterface + 27
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistry.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistry.sol.func.html index e4e938b..807e4c0 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistry.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistry.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 2 @@ -69,12 +69,12 @@ Hit count Sort by hit count - RuleIdentityRegistry._authorizeIdentityRegistryManager - 5 + RuleIdentityRegistry._authorizeIdentityRegistryManager + 12 - RuleIdentityRegistry.supportsInterface - 15 + RuleIdentityRegistry.supportsInterface + 27
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistry.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistry.sol.gcov.html index 43031da..1bda4e1 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistry.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistry.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 2 @@ -87,32 +87,45 @@ 16 : : CONSTRUCTOR 17 : : //////////////////////////////////////////////////////////////*/ 18 : : - 19 : : constructor(address admin, address identityRegistry_) - 20 : : AccessControlModuleStandalone(admin) - 21 : : RuleIdentityRegistryBase(identityRegistry_) - 22 : : {} - 23 : : - 24 : : /*////////////////////////////////////////////////////////////// - 25 : : PUBLIC FUNCTIONS - 26 : : //////////////////////////////////////////////////////////////*/ - 27 : : - 28 : 15 : function supportsInterface(bytes4 interfaceId) - 29 : : public - 30 : : view - 31 : : virtual - 32 : : override(AccessControlEnumerable, RuleTransferValidation) - 33 : : returns (bool) - 34 : : { - 35 : 15 : return AccessControlEnumerable.supportsInterface(interfaceId) - 36 : 10 : || RuleTransferValidation.supportsInterface(interfaceId); - 37 : : } - 38 : : - 39 : : /*////////////////////////////////////////////////////////////// - 40 : : ACCESS CONTROL - 41 : : //////////////////////////////////////////////////////////////*/ - 42 : : - 43 : 5 : function _authorizeIdentityRegistryManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} - 44 : : } + 19 : : /** + 20 : : * @notice Deploys the rule, sets the admin and the ERC-3643 identity registry. + 21 : : * @param admin Address that receives the default admin role. + 22 : : * @param identityRegistry_ Address of the ERC-3643 identity registry to query. + 23 : : */ + 24 : : constructor(address admin, address identityRegistry_, bool checkSender_, bool checkSpender_) + 25 : : AccessControlModuleStandalone(admin) + 26 : : RuleIdentityRegistryBase(identityRegistry_, checkSender_, checkSpender_) + 27 : : {} + 28 : : + 29 : : /*////////////////////////////////////////////////////////////// + 30 : : PUBLIC FUNCTIONS + 31 : : //////////////////////////////////////////////////////////////*/ + 32 : : + 33 : : /** + 34 : : * @notice Indicates whether this contract supports a given interface. + 35 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 36 : : * @return True if the interface is supported. + 37 : : */ + 38 : 27 : function supportsInterface(bytes4 interfaceId) + 39 : : public + 40 : : view + 41 : : virtual + 42 : : override(AccessControlEnumerable, RuleTransferValidation) + 43 : : returns (bool) + 44 : : { + 45 : 27 : return AccessControlEnumerable.supportsInterface(interfaceId) + 46 : 18 : || RuleTransferValidation.supportsInterface(interfaceId); + 47 : : } + 48 : : + 49 : : /*////////////////////////////////////////////////////////////// + 50 : : ACCESS CONTROL + 51 : : //////////////////////////////////////////////////////////////*/ + 52 : : + 53 : : /** + 54 : : * @notice Restricts identity registry management to holders of DEFAULT_ADMIN_ROLE. + 55 : : */ + 56 : 12 : function _authorizeIdentityRegistryManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + 57 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol.func-sort-c.html index 44bdad9..769a213 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 1 - 1 + 4 + 4 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 1 - 1 + 2 + 2 100.0 % @@ -69,9 +69,13 @@ Hit count Sort by hit count - RuleIdentityRegistryOwnable2Step._authorizeIdentityRegistryManager + RuleIdentityRegistryOwnable2Step._authorizeIdentityRegistryManager 4 + + RuleIdentityRegistryOwnable2Step.supportsInterface + 5 +
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol.func.html index 32ae128..311468c 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 1 - 1 + 4 + 4 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 1 - 1 + 2 + 2 100.0 % @@ -69,9 +69,13 @@ Hit count Sort by hit count - RuleIdentityRegistryOwnable2Step._authorizeIdentityRegistryManager + RuleIdentityRegistryOwnable2Step._authorizeIdentityRegistryManager 4 + + RuleIdentityRegistryOwnable2Step.supportsInterface + 5 +
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol.gcov.html index f79424b..dbf9075 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 1 - 1 + 4 + 4 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 1 - 1 + 2 + 2 100.0 % @@ -74,25 +74,58 @@ 3 : : 4 : : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 5 : : import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; - 6 : : import {RuleIdentityRegistryBase} from "../abstract/base/RuleIdentityRegistryBase.sol"; - 7 : : - 8 : : /** - 9 : : * @title RuleIdentityRegistryOwnable2Step - 10 : : * @notice Ownable2Step variant of RuleIdentityRegistry. - 11 : : */ - 12 : : contract RuleIdentityRegistryOwnable2Step is RuleIdentityRegistryBase, Ownable2Step { - 13 : : /*////////////////////////////////////////////////////////////// - 14 : : CONSTRUCTOR - 15 : : //////////////////////////////////////////////////////////////*/ - 16 : : - 17 : : constructor(address owner, address identityRegistry_) RuleIdentityRegistryBase(identityRegistry_) Ownable(owner) {} + 6 : : import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; + 7 : : import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol"; + 8 : : import {RuleIdentityRegistryBase} from "../abstract/base/RuleIdentityRegistryBase.sol"; + 9 : : + 10 : : /** + 11 : : * @title RuleIdentityRegistryOwnable2Step + 12 : : * @notice Ownable2Step variant of RuleIdentityRegistry. + 13 : : */ + 14 : : contract RuleIdentityRegistryOwnable2Step is RuleIdentityRegistryBase, Ownable2Step, Ownable2StepERC165Module { + 15 : : /*////////////////////////////////////////////////////////////// + 16 : : CONSTRUCTOR + 17 : : //////////////////////////////////////////////////////////////*/ 18 : : - 19 : : /*////////////////////////////////////////////////////////////// - 20 : : ACCESS CONTROL - 21 : : //////////////////////////////////////////////////////////////*/ - 22 : : - 23 : 4 : function _authorizeIdentityRegistryManager() internal view virtual override onlyOwner {} - 24 : : } + 19 : : /** + 20 : : * @notice Deploys the rule, sets the owner and the ERC-3643 identity registry. + 21 : : * @param owner Contract owner. + 22 : : * @param identityRegistry_ Address of the ERC-3643 identity registry to query. + 23 : : */ + 24 : : constructor(address owner, address identityRegistry_, bool checkSender_, bool checkSpender_) + 25 : : RuleIdentityRegistryBase(identityRegistry_, checkSender_, checkSpender_) + 26 : : Ownable(owner) + 27 : : {} + 28 : : + 29 : : /*////////////////////////////////////////////////////////////// + 30 : : PUBLIC FUNCTIONS + 31 : : //////////////////////////////////////////////////////////////*/ + 32 : : + 33 : : /** + 34 : : * @notice Indicates whether this contract supports a given interface. + 35 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 36 : : * @return True if the interface is supported. + 37 : : */ + 38 : 5 : function supportsInterface(bytes4 interfaceId) + 39 : : public + 40 : : view + 41 : : virtual + 42 : : override(RuleTransferValidation, Ownable2StepERC165Module) + 43 : : returns (bool) + 44 : : { + 45 : 5 : return Ownable2StepERC165Module.supportsInterface(interfaceId) + 46 : 2 : || RuleTransferValidation.supportsInterface(interfaceId); + 47 : : } + 48 : : + 49 : : /*////////////////////////////////////////////////////////////// + 50 : : ACCESS CONTROL + 51 : : //////////////////////////////////////////////////////////////*/ + 52 : : + 53 : : /** + 54 : : * @notice Restricts identity registry management to the contract owner. + 55 : : */ + 56 : 4 : function _authorizeIdentityRegistryManager() internal view virtual override onlyOwner {} + 57 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupply.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupply.sol.func-sort-c.html index 22fac2c..6e43d3f 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupply.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupply.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 2 @@ -69,11 +69,11 @@ Hit count Sort by hit count - RuleMaxTotalSupply.supportsInterface - 15 + RuleMaxTotalSupply.supportsInterface + 19 - RuleMaxTotalSupply._authorizeMaxTotalSupplyManager + RuleMaxTotalSupply._authorizeMaxTotalSupplyManager 260 diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupply.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupply.sol.func.html index a16bd35..fc1fbc3 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupply.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupply.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 2 @@ -69,12 +69,12 @@ Hit count Sort by hit count - RuleMaxTotalSupply._authorizeMaxTotalSupplyManager + RuleMaxTotalSupply._authorizeMaxTotalSupplyManager 260 - RuleMaxTotalSupply.supportsInterface - 15 + RuleMaxTotalSupply.supportsInterface + 19
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupply.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupply.sol.gcov.html index 554acc8..5f22402 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupply.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupply.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 2 @@ -100,23 +100,31 @@ 29 : : PUBLIC FUNCTIONS 30 : : //////////////////////////////////////////////////////////////*/ 31 : : - 32 : 15 : function supportsInterface(bytes4 interfaceId) - 33 : : public - 34 : : view - 35 : : virtual - 36 : : override(AccessControlEnumerable, RuleTransferValidation) - 37 : : returns (bool) - 38 : : { - 39 : 15 : return AccessControlEnumerable.supportsInterface(interfaceId) - 40 : 10 : || RuleTransferValidation.supportsInterface(interfaceId); - 41 : : } - 42 : : - 43 : : /*////////////////////////////////////////////////////////////// - 44 : : ACCESS CONTROL - 45 : : //////////////////////////////////////////////////////////////*/ - 46 : : - 47 : 260 : function _authorizeMaxTotalSupplyManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} - 48 : : } + 32 : : /** + 33 : : * @notice Indicates whether this contract supports a given interface. + 34 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 35 : : * @return True if the interface is supported. + 36 : : */ + 37 : 19 : function supportsInterface(bytes4 interfaceId) + 38 : : public + 39 : : view + 40 : : virtual + 41 : : override(AccessControlEnumerable, RuleTransferValidation) + 42 : : returns (bool) + 43 : : { + 44 : 19 : return AccessControlEnumerable.supportsInterface(interfaceId) + 45 : 13 : || RuleTransferValidation.supportsInterface(interfaceId); + 46 : : } + 47 : : + 48 : : /*////////////////////////////////////////////////////////////// + 49 : : ACCESS CONTROL + 50 : : //////////////////////////////////////////////////////////////*/ + 51 : : + 52 : : /** + 53 : : * @notice Restricts maximum total supply management to holders of DEFAULT_ADMIN_ROLE. + 54 : : */ + 55 : 260 : function _authorizeMaxTotalSupplyManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + 56 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol.func-sort-c.html index 40d1ef8..2a0b0ca 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 1 - 1 + 4 + 4 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 1 - 1 + 2 + 2 100.0 % @@ -69,9 +69,13 @@ Hit count Sort by hit count - RuleMaxTotalSupplyOwnable2Step._authorizeMaxTotalSupplyManager + RuleMaxTotalSupplyOwnable2Step._authorizeMaxTotalSupplyManager 4 + + RuleMaxTotalSupplyOwnable2Step.supportsInterface + 5 +
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol.func.html index 8f80584..5499213 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 1 - 1 + 4 + 4 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 1 - 1 + 2 + 2 100.0 % @@ -69,9 +69,13 @@ Hit count Sort by hit count - RuleMaxTotalSupplyOwnable2Step._authorizeMaxTotalSupplyManager + RuleMaxTotalSupplyOwnable2Step._authorizeMaxTotalSupplyManager 4 + + RuleMaxTotalSupplyOwnable2Step.supportsInterface + 5 +
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol.gcov.html index 9bef84f..0d66674 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 1 - 1 + 4 + 4 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 1 - 1 + 2 + 2 100.0 % @@ -74,28 +74,59 @@ 3 : : 4 : : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 5 : : import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; - 6 : : import {RuleMaxTotalSupplyBase} from "../abstract/base/RuleMaxTotalSupplyBase.sol"; - 7 : : - 8 : : /** - 9 : : * @title RuleMaxTotalSupplyOwnable2Step - 10 : : * @notice Ownable2Step variant of RuleMaxTotalSupply. - 11 : : */ - 12 : : contract RuleMaxTotalSupplyOwnable2Step is RuleMaxTotalSupplyBase, Ownable2Step { - 13 : : /*////////////////////////////////////////////////////////////// - 14 : : CONSTRUCTOR - 15 : : //////////////////////////////////////////////////////////////*/ - 16 : : - 17 : : constructor(address owner, address tokenContract_, uint256 maxTotalSupply_) - 18 : : RuleMaxTotalSupplyBase(tokenContract_, maxTotalSupply_) - 19 : : Ownable(owner) - 20 : : {} - 21 : : - 22 : : /*////////////////////////////////////////////////////////////// - 23 : : ACCESS CONTROL - 24 : : //////////////////////////////////////////////////////////////*/ - 25 : : - 26 : 4 : function _authorizeMaxTotalSupplyManager() internal view virtual override onlyOwner {} - 27 : : } + 6 : : import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; + 7 : : import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol"; + 8 : : import {RuleMaxTotalSupplyBase} from "../abstract/base/RuleMaxTotalSupplyBase.sol"; + 9 : : + 10 : : /** + 11 : : * @title RuleMaxTotalSupplyOwnable2Step + 12 : : * @notice Ownable2Step variant of RuleMaxTotalSupply. + 13 : : */ + 14 : : contract RuleMaxTotalSupplyOwnable2Step is RuleMaxTotalSupplyBase, Ownable2Step, Ownable2StepERC165Module { + 15 : : /*////////////////////////////////////////////////////////////// + 16 : : CONSTRUCTOR + 17 : : //////////////////////////////////////////////////////////////*/ + 18 : : + 19 : : /** + 20 : : * @notice Deploys the rule, sets the owner, the token contract and the initial maximum supply. + 21 : : * @param owner Contract owner. + 22 : : * @param tokenContract_ Token contract that exposes totalSupply (must be non-zero). + 23 : : * @param maxTotalSupply_ Initial maximum supply. + 24 : : */ + 25 : : constructor(address owner, address tokenContract_, uint256 maxTotalSupply_) + 26 : : RuleMaxTotalSupplyBase(tokenContract_, maxTotalSupply_) + 27 : : Ownable(owner) + 28 : : {} + 29 : : + 30 : : /*////////////////////////////////////////////////////////////// + 31 : : PUBLIC FUNCTIONS + 32 : : //////////////////////////////////////////////////////////////*/ + 33 : : + 34 : : /** + 35 : : * @notice Indicates whether this contract supports a given interface. + 36 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 37 : : * @return True if the interface is supported. + 38 : : */ + 39 : 5 : function supportsInterface(bytes4 interfaceId) + 40 : : public + 41 : : view + 42 : : virtual + 43 : : override(RuleTransferValidation, Ownable2StepERC165Module) + 44 : : returns (bool) + 45 : : { + 46 : 5 : return Ownable2StepERC165Module.supportsInterface(interfaceId) + 47 : 2 : || RuleTransferValidation.supportsInterface(interfaceId); + 48 : : } + 49 : : + 50 : : /*////////////////////////////////////////////////////////////// + 51 : : ACCESS CONTROL + 52 : : //////////////////////////////////////////////////////////////*/ + 53 : : + 54 : : /** + 55 : : * @notice Restricts maximum total supply management to the contract owner. + 56 : : */ + 57 : 4 : function _authorizeMaxTotalSupplyManager() internal view virtual override onlyOwner {} + 58 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsList.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsList.sol.func-sort-c.html index 97f8b5e..4d18770 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsList.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsList.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 5 @@ -69,24 +69,24 @@ Hit count Sort by hit count - RuleSanctionsList._msgData + RuleSanctionsList._msgData 1 - RuleSanctionsList._authorizeSanctionListManager - 17 + RuleSanctionsList._authorizeSanctionListManager + 18 - RuleSanctionsList._msgSender - 57 + RuleSanctionsList.supportsInterface + 58 - RuleSanctionsList._contextSuffixLength - 58 + RuleSanctionsList._msgSender + 60 - RuleSanctionsList.supportsInterface - 58 + RuleSanctionsList._contextSuffixLength + 61
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsList.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsList.sol.func.html index 3bfcb8d..69aa75b 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsList.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsList.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 5 @@ -69,23 +69,23 @@ Hit count Sort by hit count - RuleSanctionsList._authorizeSanctionListManager - 17 + RuleSanctionsList._authorizeSanctionListManager + 18 - RuleSanctionsList._contextSuffixLength - 58 + RuleSanctionsList._contextSuffixLength + 61 - RuleSanctionsList._msgData + RuleSanctionsList._msgData 1 - RuleSanctionsList._msgSender - 57 + RuleSanctionsList._msgSender + 60 - RuleSanctionsList.supportsInterface + RuleSanctionsList.supportsInterface 58 diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsList.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsList.sol.gcov.html index c0e638d..82be194 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsList.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsList.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 5 @@ -92,49 +92,70 @@ 21 : : /** 22 : : * @param admin Address of the contract (Access Control) 23 : : * @param forwarderIrrevocable Address of the forwarder, required for the gasless support - 24 : : */ - 25 : : constructor(address admin, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) - 26 : : AccessControlModuleStandalone(admin) - 27 : : RuleSanctionsListBase(forwarderIrrevocable, sanctionContractOracle_) - 28 : : {} - 29 : : - 30 : : /*////////////////////////////////////////////////////////////// - 31 : : PUBLIC FUNCTIONS - 32 : : //////////////////////////////////////////////////////////////*/ - 33 : : - 34 : 58 : function supportsInterface(bytes4 interfaceId) - 35 : : public - 36 : : view - 37 : : virtual - 38 : : override(AccessControlEnumerable, RuleTransferValidation) - 39 : : returns (bool) - 40 : : { - 41 : 58 : return AccessControlEnumerable.supportsInterface(interfaceId) - 42 : 39 : || RuleTransferValidation.supportsInterface(interfaceId); - 43 : : } - 44 : : - 45 : : /*////////////////////////////////////////////////////////////// - 46 : : ACCESS CONTROL - 47 : : //////////////////////////////////////////////////////////////*/ - 48 : : - 49 : 17 : function _authorizeSanctionListManager() internal view virtual override onlyRole(SANCTIONLIST_ROLE) {} + 24 : : * @param sanctionContractOracle_ Chainalysis sanctions oracle used to screen addresses + 25 : : */ + 26 : : constructor(address admin, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) + 27 : : AccessControlModuleStandalone(admin) + 28 : : RuleSanctionsListBase(forwarderIrrevocable, sanctionContractOracle_) + 29 : : {} + 30 : : + 31 : : /*////////////////////////////////////////////////////////////// + 32 : : PUBLIC FUNCTIONS + 33 : : //////////////////////////////////////////////////////////////*/ + 34 : : + 35 : : /** + 36 : : * @notice Indicates whether this contract supports a given interface. + 37 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 38 : : * @return True if the interface is supported. + 39 : : */ + 40 : 58 : function supportsInterface(bytes4 interfaceId) + 41 : : public + 42 : : view + 43 : : virtual + 44 : : override(AccessControlEnumerable, RuleTransferValidation) + 45 : : returns (bool) + 46 : : { + 47 : 58 : return AccessControlEnumerable.supportsInterface(interfaceId) + 48 : 39 : || RuleTransferValidation.supportsInterface(interfaceId); + 49 : : } 50 : : 51 : : /*////////////////////////////////////////////////////////////// - 52 : : INTERNAL FUNCTIONS + 52 : : ACCESS CONTROL 53 : : //////////////////////////////////////////////////////////////*/ 54 : : - 55 : 57 : function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { - 56 : 57 : return ERC2771Context._msgSender(); - 57 : : } - 58 : : - 59 : 1 : function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { - 60 : 1 : return ERC2771Context._msgData(); - 61 : : } - 62 : : - 63 : 58 : function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { - 64 : 58 : return ERC2771Context._contextSuffixLength(); - 65 : : } - 66 : : } + 55 : : /** + 56 : : * @notice Restricts sanctions list management to holders of SANCTIONLIST_ROLE. + 57 : : */ + 58 : 18 : function _authorizeSanctionListManager() internal view virtual override onlyRole(SANCTIONLIST_ROLE) {} + 59 : : + 60 : : /*////////////////////////////////////////////////////////////// + 61 : : INTERNAL FUNCTIONS + 62 : : //////////////////////////////////////////////////////////////*/ + 63 : : + 64 : : /** + 65 : : * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + 66 : : * @return sender The address of the message sender. + 67 : : */ + 68 : 60 : function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { + 69 : 60 : return ERC2771Context._msgSender(); + 70 : : } + 71 : : + 72 : : /** + 73 : : * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + 74 : : * @return The message calldata. + 75 : : */ + 76 : 1 : function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { + 77 : 1 : return ERC2771Context._msgData(); + 78 : : } + 79 : : + 80 : : /** + 81 : : * @notice Returns the length of the context suffix appended by the forwarder. + 82 : : * @return The context suffix length in bytes. + 83 : : */ + 84 : 61 : function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { + 85 : 61 : return ERC2771Context._contextSuffixLength(); + 86 : : } + 87 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol.func-sort-c.html index 3da8d95..59c428d 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 7 - 7 + 10 + 10 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 4 - 4 + 5 + 5 100.0 % @@ -69,19 +69,23 @@ Hit count Sort by hit count - RuleSanctionsListOwnable2Step._msgData + RuleSanctionsListOwnable2Step._msgData 1 - RuleSanctionsListOwnable2Step._authorizeSanctionListManager + RuleSanctionsListOwnable2Step._authorizeSanctionListManager 3 - RuleSanctionsListOwnable2Step._msgSender + RuleSanctionsListOwnable2Step.supportsInterface + 5 + + + RuleSanctionsListOwnable2Step._msgSender 11 - RuleSanctionsListOwnable2Step._contextSuffixLength + RuleSanctionsListOwnable2Step._contextSuffixLength 13 diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol.func.html index a525d43..ef35521 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 7 - 7 + 10 + 10 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 4 - 4 + 5 + 5 100.0 % @@ -69,21 +69,25 @@ Hit count Sort by hit count - RuleSanctionsListOwnable2Step._authorizeSanctionListManager + RuleSanctionsListOwnable2Step._authorizeSanctionListManager 3 - RuleSanctionsListOwnable2Step._contextSuffixLength + RuleSanctionsListOwnable2Step._contextSuffixLength 13 - RuleSanctionsListOwnable2Step._msgData + RuleSanctionsListOwnable2Step._msgData 1 - RuleSanctionsListOwnable2Step._msgSender + RuleSanctionsListOwnable2Step._msgSender 11 + + RuleSanctionsListOwnable2Step.supportsInterface + 5 +
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol.gcov.html index 9b53258..73388d3 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 7 - 7 + 10 + 10 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 4 - 4 + 5 + 5 100.0 % @@ -75,46 +75,89 @@ 4 : : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 5 : : import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; 6 : : import {Context} from "@openzeppelin/contracts/utils/Context.sol"; - 7 : : import {ERC2771Context} from "../../../modules/MetaTxModuleStandalone.sol"; - 8 : : import {RuleSanctionsListBase} from "../abstract/base/RuleSanctionsListBase.sol"; - 9 : : import {ISanctionsList} from "../../interfaces/ISanctionsList.sol"; - 10 : : - 11 : : /** - 12 : : * @title RuleSanctionsListOwnable2Step - 13 : : * @notice Ownable2Step variant of RuleSanctionsList. - 14 : : */ - 15 : : contract RuleSanctionsListOwnable2Step is RuleSanctionsListBase, Ownable2Step { - 16 : : /*////////////////////////////////////////////////////////////// - 17 : : CONSTRUCTOR - 18 : : //////////////////////////////////////////////////////////////*/ - 19 : : - 20 : : constructor(address owner, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) - 21 : : RuleSanctionsListBase(forwarderIrrevocable, sanctionContractOracle_) - 22 : : Ownable(owner) - 23 : : {} - 24 : : - 25 : : /*////////////////////////////////////////////////////////////// - 26 : : ACCESS CONTROL - 27 : : //////////////////////////////////////////////////////////////*/ - 28 : : - 29 : 3 : function _authorizeSanctionListManager() internal view virtual override onlyOwner {} - 30 : : - 31 : : /*////////////////////////////////////////////////////////////// - 32 : : INTERNAL FUNCTIONS - 33 : : //////////////////////////////////////////////////////////////*/ - 34 : : - 35 : 11 : function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { - 36 : 11 : return ERC2771Context._msgSender(); - 37 : : } - 38 : : - 39 : 1 : function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { - 40 : 1 : return ERC2771Context._msgData(); - 41 : : } - 42 : : - 43 : 13 : function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { - 44 : 13 : return ERC2771Context._contextSuffixLength(); - 45 : : } - 46 : : } + 7 : : import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; + 8 : : import {ERC2771Context} from "../../../modules/MetaTxModuleStandalone.sol"; + 9 : : import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol"; + 10 : : import {RuleSanctionsListBase} from "../abstract/base/RuleSanctionsListBase.sol"; + 11 : : import {ISanctionsList} from "../../interfaces/ISanctionsList.sol"; + 12 : : + 13 : : /** + 14 : : * @title RuleSanctionsListOwnable2Step + 15 : : * @notice Ownable2Step variant of RuleSanctionsList. + 16 : : */ + 17 : : contract RuleSanctionsListOwnable2Step is RuleSanctionsListBase, Ownable2Step, Ownable2StepERC165Module { + 18 : : /*////////////////////////////////////////////////////////////// + 19 : : CONSTRUCTOR + 20 : : //////////////////////////////////////////////////////////////*/ + 21 : : + 22 : : /** + 23 : : * @notice Deploys the rule, sets the owner, the forwarder and the sanctions oracle. + 24 : : * @param owner Contract owner. + 25 : : * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. + 26 : : * @param sanctionContractOracle_ Chainalysis sanctions oracle used to screen addresses. + 27 : : */ + 28 : : constructor(address owner, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) + 29 : : RuleSanctionsListBase(forwarderIrrevocable, sanctionContractOracle_) + 30 : : Ownable(owner) + 31 : : {} + 32 : : + 33 : : /*////////////////////////////////////////////////////////////// + 34 : : PUBLIC FUNCTIONS + 35 : : //////////////////////////////////////////////////////////////*/ + 36 : : + 37 : : /** + 38 : : * @notice Indicates whether this contract supports a given interface. + 39 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 40 : : * @return True if the interface is supported. + 41 : : */ + 42 : 5 : function supportsInterface(bytes4 interfaceId) + 43 : : public + 44 : : view + 45 : : virtual + 46 : : override(RuleTransferValidation, Ownable2StepERC165Module) + 47 : : returns (bool) + 48 : : { + 49 : 5 : return Ownable2StepERC165Module.supportsInterface(interfaceId) + 50 : 2 : || RuleTransferValidation.supportsInterface(interfaceId); + 51 : : } + 52 : : + 53 : : /*////////////////////////////////////////////////////////////// + 54 : : ACCESS CONTROL + 55 : : //////////////////////////////////////////////////////////////*/ + 56 : : + 57 : : /** + 58 : : * @notice Restricts sanctions list management to the contract owner. + 59 : : */ + 60 : 3 : function _authorizeSanctionListManager() internal view virtual override onlyOwner {} + 61 : : + 62 : : /*////////////////////////////////////////////////////////////// + 63 : : INTERNAL FUNCTIONS + 64 : : //////////////////////////////////////////////////////////////*/ + 65 : : + 66 : : /** + 67 : : * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + 68 : : * @return sender The address of the message sender. + 69 : : */ + 70 : 11 : function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { + 71 : 11 : return ERC2771Context._msgSender(); + 72 : : } + 73 : : + 74 : : /** + 75 : : * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + 76 : : * @return The message calldata. + 77 : : */ + 78 : 1 : function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { + 79 : 1 : return ERC2771Context._msgData(); + 80 : : } + 81 : : + 82 : : /** + 83 : : * @notice Returns the length of the context suffix appended by the forwarder. + 84 : : * @return The context suffix length in bytes. + 85 : : */ + 86 : 13 : function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { + 87 : 13 : return ERC2771Context._contextSuffixLength(); + 88 : : } + 89 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelist.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelist.sol.func-sort-c.html index 07e51e2..99ca642 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelist.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelist.sol.func-sort-c.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 6 @@ -69,28 +69,28 @@ Hit count Sort by hit count - RuleSpenderWhitelist._msgData + RuleSpenderWhitelist._msgData 1 - RuleSpenderWhitelist._authorizeAddressListRemove + RuleSpenderWhitelist._authorizeAddressListRemove 2 - RuleSpenderWhitelist.supportsInterface - 5 + RuleSpenderWhitelist.supportsInterface + 6 - RuleSpenderWhitelist._authorizeAddressListAdd - 6 + RuleSpenderWhitelist._authorizeAddressListAdd + 7 - RuleSpenderWhitelist._msgSender - 31 + RuleSpenderWhitelist._msgSender + 38 - RuleSpenderWhitelist._contextSuffixLength - 33 + RuleSpenderWhitelist._contextSuffixLength + 40
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelist.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelist.sol.func.html index 8028e78..352ef62 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelist.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelist.sol.func.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 6 @@ -69,28 +69,28 @@ Hit count Sort by hit count - RuleSpenderWhitelist._authorizeAddressListAdd - 6 + RuleSpenderWhitelist._authorizeAddressListAdd + 7 - RuleSpenderWhitelist._authorizeAddressListRemove + RuleSpenderWhitelist._authorizeAddressListRemove 2 - RuleSpenderWhitelist._contextSuffixLength - 33 + RuleSpenderWhitelist._contextSuffixLength + 40 - RuleSpenderWhitelist._msgData + RuleSpenderWhitelist._msgData 1 - RuleSpenderWhitelist._msgSender - 31 + RuleSpenderWhitelist._msgSender + 38 - RuleSpenderWhitelist.supportsInterface - 5 + RuleSpenderWhitelist.supportsInterface + 6
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelist.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelist.sol.gcov.html index d9dd156..63bbc74 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelist.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelist.sol.gcov.html @@ -37,7 +37,7 @@ Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: 6 @@ -77,61 +77,88 @@ 6 : : import {AccessControlModuleStandalone} from "../../../modules/AccessControlModuleStandalone.sol"; 7 : : import {RuleSpenderWhitelistBase} from "../abstract/base/RuleSpenderWhitelistBase.sol"; 8 : : import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; - 9 : : import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol"; - 10 : : - 11 : : /** - 12 : : * @title RuleSpenderWhitelist - 13 : : * @notice AccessControlEnumerable deployment variant of spender whitelist rule. - 14 : : */ - 15 : : contract RuleSpenderWhitelist is RuleSpenderWhitelistBase, AccessControlModuleStandalone { - 16 : : /*////////////////////////////////////////////////////////////// - 17 : : CONSTRUCTOR - 18 : : //////////////////////////////////////////////////////////////*/ - 19 : : - 20 : : constructor(address admin, address forwarderIrrevocable) - 21 : : RuleSpenderWhitelistBase(forwarderIrrevocable) - 22 : : AccessControlModuleStandalone(admin) - 23 : : {} - 24 : : - 25 : : /*////////////////////////////////////////////////////////////// - 26 : : PUBLIC FUNCTIONS - 27 : : //////////////////////////////////////////////////////////////*/ + 9 : : + 10 : : /** + 11 : : * @title RuleSpenderWhitelist + 12 : : * @notice AccessControlEnumerable deployment variant of spender whitelist rule. + 13 : : */ + 14 : : contract RuleSpenderWhitelist is RuleSpenderWhitelistBase, AccessControlModuleStandalone { + 15 : : /*////////////////////////////////////////////////////////////// + 16 : : CONSTRUCTOR + 17 : : //////////////////////////////////////////////////////////////*/ + 18 : : + 19 : : /** + 20 : : * @notice Deploys the rule, sets the admin and the meta-transaction forwarder. + 21 : : * @param admin Address that receives the default admin role. + 22 : : * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. + 23 : : */ + 24 : : constructor(address admin, address forwarderIrrevocable) + 25 : : RuleSpenderWhitelistBase(forwarderIrrevocable) + 26 : : AccessControlModuleStandalone(admin) + 27 : : {} 28 : : - 29 : 5 : function supportsInterface(bytes4 interfaceId) - 30 : : public - 31 : : view - 32 : : virtual - 33 : : override(AccessControlEnumerable, RuleTransferValidation) - 34 : : returns (bool) - 35 : : { - 36 : 5 : return AccessControlEnumerable.supportsInterface(interfaceId) - 37 : 4 : || RuleTransferValidation.supportsInterface(interfaceId); - 38 : : } - 39 : : - 40 : : /*////////////////////////////////////////////////////////////// - 41 : : ACCESS CONTROL - 42 : : //////////////////////////////////////////////////////////////*/ - 43 : : - 44 : 6 : function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} - 45 : : - 46 : 2 : function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} - 47 : : - 48 : : /*////////////////////////////////////////////////////////////// - 49 : : INTERNAL FUNCTIONS - 50 : : //////////////////////////////////////////////////////////////*/ - 51 : : - 52 : 31 : function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { - 53 : 31 : return super._msgSender(); - 54 : : } - 55 : : - 56 : 1 : function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { - 57 : 1 : return super._msgData(); - 58 : : } - 59 : : - 60 : 33 : function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { - 61 : 33 : return super._contextSuffixLength(); - 62 : : } - 63 : : } + 29 : : /*////////////////////////////////////////////////////////////// + 30 : : PUBLIC FUNCTIONS + 31 : : //////////////////////////////////////////////////////////////*/ + 32 : : + 33 : : /** + 34 : : * @notice Indicates whether this contract supports a given interface. + 35 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 36 : : * @return True if the interface is supported. + 37 : : */ + 38 : 6 : function supportsInterface(bytes4 interfaceId) + 39 : : public + 40 : : view + 41 : : virtual + 42 : : override(AccessControlEnumerable, RuleSpenderWhitelistBase) + 43 : : returns (bool) + 44 : : { + 45 : 6 : return AccessControlEnumerable.supportsInterface(interfaceId) + 46 : 5 : || RuleSpenderWhitelistBase.supportsInterface(interfaceId); + 47 : : } + 48 : : + 49 : : /*////////////////////////////////////////////////////////////// + 50 : : ACCESS CONTROL + 51 : : //////////////////////////////////////////////////////////////*/ + 52 : : + 53 : : /** + 54 : : * @notice Restricts adding addresses to the spender whitelist to holders of ADDRESS_LIST_ADD_ROLE. + 55 : : */ + 56 : 7 : function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + 57 : : + 58 : : /** + 59 : : * @notice Restricts removing addresses from the spender whitelist to holders of ADDRESS_LIST_REMOVE_ROLE. + 60 : : */ + 61 : 2 : function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + 62 : : + 63 : : /*////////////////////////////////////////////////////////////// + 64 : : INTERNAL FUNCTIONS + 65 : : //////////////////////////////////////////////////////////////*/ + 66 : : + 67 : : /** + 68 : : * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + 69 : : * @return sender The address of the message sender. + 70 : : */ + 71 : 38 : function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { + 72 : 38 : return super._msgSender(); + 73 : : } + 74 : : + 75 : : /** + 76 : : * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + 77 : : * @return The message calldata. + 78 : : */ + 79 : 1 : function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { + 80 : 1 : return super._msgData(); + 81 : : } + 82 : : + 83 : : /** + 84 : : * @notice Returns the length of the context suffix appended by the forwarder. + 85 : : * @return The context suffix length in bytes. + 86 : : */ + 87 : 40 : function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { + 88 : 40 : return super._contextSuffixLength(); + 89 : : } + 90 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol.func-sort-c.html index c10953b..743728e 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 8 - 8 + 11 + 11 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 5 - 5 + 6 + 6 100.0 % @@ -69,23 +69,27 @@ Hit count Sort by hit count - RuleSpenderWhitelistOwnable2Step._msgData + RuleSpenderWhitelistOwnable2Step._msgData 1 - RuleSpenderWhitelistOwnable2Step._authorizeAddressListAdd + RuleSpenderWhitelistOwnable2Step._authorizeAddressListAdd 2 - RuleSpenderWhitelistOwnable2Step._authorizeAddressListRemove + RuleSpenderWhitelistOwnable2Step._authorizeAddressListRemove 2 - RuleSpenderWhitelistOwnable2Step._msgSender + RuleSpenderWhitelistOwnable2Step.supportsInterface + 6 + + + RuleSpenderWhitelistOwnable2Step._msgSender 10 - RuleSpenderWhitelistOwnable2Step._contextSuffixLength + RuleSpenderWhitelistOwnable2Step._contextSuffixLength 12 diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol.func.html index 6d0abf0..f0afa4c 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 8 - 8 + 11 + 11 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 5 - 5 + 6 + 6 100.0 % @@ -69,25 +69,29 @@ Hit count Sort by hit count - RuleSpenderWhitelistOwnable2Step._authorizeAddressListAdd + RuleSpenderWhitelistOwnable2Step._authorizeAddressListAdd 2 - RuleSpenderWhitelistOwnable2Step._authorizeAddressListRemove + RuleSpenderWhitelistOwnable2Step._authorizeAddressListRemove 2 - RuleSpenderWhitelistOwnable2Step._contextSuffixLength + RuleSpenderWhitelistOwnable2Step._contextSuffixLength 12 - RuleSpenderWhitelistOwnable2Step._msgData + RuleSpenderWhitelistOwnable2Step._msgData 1 - RuleSpenderWhitelistOwnable2Step._msgSender + RuleSpenderWhitelistOwnable2Step._msgSender 10 + + RuleSpenderWhitelistOwnable2Step.supportsInterface + 6 +
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol.gcov.html index 4e5cbf7..b97e62e 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 8 - 8 + 11 + 11 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 5 - 5 + 6 + 6 100.0 % @@ -75,47 +75,91 @@ 4 : : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 5 : : import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; 6 : : import {Context} from "@openzeppelin/contracts/utils/Context.sol"; - 7 : : import {RuleSpenderWhitelistBase} from "../abstract/base/RuleSpenderWhitelistBase.sol"; - 8 : : import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; - 9 : : - 10 : : /** - 11 : : * @title RuleSpenderWhitelistOwnable2Step - 12 : : * @notice Ownable2Step deployment variant of spender whitelist rule. - 13 : : */ - 14 : : contract RuleSpenderWhitelistOwnable2Step is RuleSpenderWhitelistBase, Ownable2Step { - 15 : : /*////////////////////////////////////////////////////////////// - 16 : : CONSTRUCTOR - 17 : : //////////////////////////////////////////////////////////////*/ - 18 : : - 19 : : constructor(address owner, address forwarderIrrevocable) - 20 : : RuleSpenderWhitelistBase(forwarderIrrevocable) - 21 : : Ownable(owner) - 22 : : {} - 23 : : - 24 : : /*////////////////////////////////////////////////////////////// - 25 : : ACCESS CONTROL - 26 : : //////////////////////////////////////////////////////////////*/ - 27 : : - 28 : 2 : function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + 7 : : import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; + 8 : : import {RuleSpenderWhitelistBase} from "../abstract/base/RuleSpenderWhitelistBase.sol"; + 9 : : import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; + 10 : : + 11 : : /** + 12 : : * @title RuleSpenderWhitelistOwnable2Step + 13 : : * @notice Ownable2Step deployment variant of spender whitelist rule. + 14 : : */ + 15 : : contract RuleSpenderWhitelistOwnable2Step is RuleSpenderWhitelistBase, Ownable2Step, Ownable2StepERC165Module { + 16 : : /*////////////////////////////////////////////////////////////// + 17 : : CONSTRUCTOR + 18 : : //////////////////////////////////////////////////////////////*/ + 19 : : + 20 : : /** + 21 : : * @notice Deploys the rule, sets the owner and the meta-transaction forwarder. + 22 : : * @param owner Contract owner. + 23 : : * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. + 24 : : */ + 25 : : constructor(address owner, address forwarderIrrevocable) + 26 : : RuleSpenderWhitelistBase(forwarderIrrevocable) + 27 : : Ownable(owner) + 28 : : {} 29 : : - 30 : 2 : function _authorizeAddressListRemove() internal view virtual override onlyOwner {} - 31 : : - 32 : : /*////////////////////////////////////////////////////////////// - 33 : : INTERNAL FUNCTIONS - 34 : : //////////////////////////////////////////////////////////////*/ - 35 : : - 36 : 10 : function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { - 37 : 10 : return super._msgSender(); - 38 : : } - 39 : : - 40 : 1 : function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { - 41 : 1 : return super._msgData(); - 42 : : } - 43 : : - 44 : 12 : function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { - 45 : 12 : return super._contextSuffixLength(); - 46 : : } - 47 : : } + 30 : : /*////////////////////////////////////////////////////////////// + 31 : : PUBLIC FUNCTIONS + 32 : : //////////////////////////////////////////////////////////////*/ + 33 : : + 34 : : /** + 35 : : * @notice Indicates whether this contract supports a given interface. + 36 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 37 : : * @return True if the interface is supported. + 38 : : */ + 39 : 6 : function supportsInterface(bytes4 interfaceId) + 40 : : public + 41 : : view + 42 : : virtual + 43 : : override(RuleSpenderWhitelistBase, Ownable2StepERC165Module) + 44 : : returns (bool) + 45 : : { + 46 : 6 : return Ownable2StepERC165Module.supportsInterface(interfaceId) + 47 : 3 : || RuleSpenderWhitelistBase.supportsInterface(interfaceId); + 48 : : } + 49 : : + 50 : : /*////////////////////////////////////////////////////////////// + 51 : : ACCESS CONTROL + 52 : : //////////////////////////////////////////////////////////////*/ + 53 : : + 54 : : /** + 55 : : * @notice Restricts adding addresses to the spender whitelist to the contract owner. + 56 : : */ + 57 : 2 : function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + 58 : : + 59 : : /** + 60 : : * @notice Restricts removing addresses from the spender whitelist to the contract owner. + 61 : : */ + 62 : 2 : function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + 63 : : + 64 : : /*////////////////////////////////////////////////////////////// + 65 : : INTERNAL FUNCTIONS + 66 : : //////////////////////////////////////////////////////////////*/ + 67 : : + 68 : : /** + 69 : : * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + 70 : : * @return sender The address of the message sender. + 71 : : */ + 72 : 10 : function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { + 73 : 10 : return super._msgSender(); + 74 : : } + 75 : : + 76 : : /** + 77 : : * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + 78 : : * @return The message calldata. + 79 : : */ + 80 : 1 : function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { + 81 : 1 : return super._msgData(); + 82 : : } + 83 : : + 84 : : /** + 85 : : * @notice Returns the length of the context suffix appended by the forwarder. + 86 : : * @return The context suffix length in bytes. + 87 : : */ + 88 : 12 : function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { + 89 : 12 : return super._contextSuffixLength(); + 90 : : } + 91 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelist.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelist.sol.func-sort-c.html index fd55174..6578dde 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelist.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelist.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 12 - 12 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 7 - 7 + 8 + 8 100.0 % @@ -69,32 +69,36 @@ Hit count Sort by hit count - RuleWhitelist._authorizeCheckSpenderManager + RuleWhitelist._authorizeCheckSpenderManager 1 - RuleWhitelist._msgData + RuleWhitelist._msgData 1 + + RuleWhitelist._authorizeMintBurnManager + 10 + RuleWhitelist.supportsInterface - 40 + 47 - RuleWhitelist._authorizeAddressListRemove + RuleWhitelist._authorizeAddressListRemove 263 - RuleWhitelist._authorizeAddressListAdd - 352 + RuleWhitelist._authorizeAddressListAdd + 367 - RuleWhitelist._msgSender - 780 + RuleWhitelist._msgSender + 827 - RuleWhitelist._contextSuffixLength - 781 + RuleWhitelist._contextSuffixLength + 828
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelist.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelist.sol.func.html index db22430..43bdefe 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelist.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelist.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 12 - 12 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 7 - 7 + 8 + 8 100.0 % @@ -69,32 +69,36 @@ Hit count Sort by hit count - RuleWhitelist._authorizeAddressListAdd - 352 + RuleWhitelist._authorizeAddressListAdd + 367 - RuleWhitelist._authorizeAddressListRemove + RuleWhitelist._authorizeAddressListRemove 263 - RuleWhitelist._authorizeCheckSpenderManager + RuleWhitelist._authorizeCheckSpenderManager 1 - RuleWhitelist._contextSuffixLength - 781 + RuleWhitelist._authorizeMintBurnManager + 10 - RuleWhitelist._msgData + RuleWhitelist._contextSuffixLength + 828 + + + RuleWhitelist._msgData 1 - RuleWhitelist._msgSender - 780 + RuleWhitelist._msgSender + 827 RuleWhitelist.supportsInterface - 40 + 47
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelist.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelist.sol.gcov.html index 130ea73..32857fe 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelist.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelist.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 12 - 12 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 7 - 7 + 8 + 8 100.0 % @@ -115,43 +115,69 @@ 44 : : * @param interfaceId The interface identifier, as specified in ERC-165. 45 : : * @return supported True if the interface is supported. 46 : : */ - 47 : 40 : function supportsInterface(bytes4 interfaceId) + 47 : 47 : function supportsInterface(bytes4 interfaceId) 48 : : public 49 : : view 50 : : virtual 51 : : override(AccessControlEnumerable, RuleWhitelistBase) 52 : : returns (bool) 53 : : { - 54 : 40 : return AccessControlEnumerable.supportsInterface(interfaceId) - 55 : 27 : || RuleWhitelistBase.supportsInterface(interfaceId); + 54 : 47 : return AccessControlEnumerable.supportsInterface(interfaceId) + 55 : 32 : || RuleWhitelistBase.supportsInterface(interfaceId); 56 : : } 57 : : 58 : : /*////////////////////////////////////////////////////////////// 59 : : ACCESS CONTROL 60 : : //////////////////////////////////////////////////////////////*/ 61 : : - 62 : 1 : function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} - 63 : : - 64 : 352 : function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} - 65 : : - 66 : 263 : function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} - 67 : : - 68 : : /*////////////////////////////////////////////////////////////// - 69 : : INTERNAL FUNCTIONS - 70 : : //////////////////////////////////////////////////////////////*/ + 62 : : /** + 63 : : * @notice Restricts toggling the spender-check setting to holders of DEFAULT_ADMIN_ROLE. + 64 : : */ + 65 : 1 : function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + 66 : : + 67 : : /** + 68 : : * @notice Restricts toggling `allowMint` / `allowBurn` to holders of DEFAULT_ADMIN_ROLE. + 69 : : */ + 70 : 10 : function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} 71 : : - 72 : 780 : function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { - 73 : 780 : return super._msgSender(); - 74 : : } - 75 : : - 76 : 1 : function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { - 77 : 1 : return super._msgData(); - 78 : : } - 79 : : - 80 : 781 : function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { - 81 : 781 : return super._contextSuffixLength(); - 82 : : } - 83 : : } + 72 : : /** + 73 : : * @notice Restricts adding addresses to the whitelist to holders of ADDRESS_LIST_ADD_ROLE. + 74 : : */ + 75 : 367 : function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + 76 : : + 77 : : /** + 78 : : * @notice Restricts removing addresses from the whitelist to holders of ADDRESS_LIST_REMOVE_ROLE. + 79 : : */ + 80 : 263 : function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + 81 : : + 82 : : /*////////////////////////////////////////////////////////////// + 83 : : INTERNAL FUNCTIONS + 84 : : //////////////////////////////////////////////////////////////*/ + 85 : : + 86 : : /** + 87 : : * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + 88 : : * @return sender The address of the message sender. + 89 : : */ + 90 : 827 : function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { + 91 : 827 : return super._msgSender(); + 92 : : } + 93 : : + 94 : : /** + 95 : : * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + 96 : : * @return The message calldata. + 97 : : */ + 98 : 1 : function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { + 99 : 1 : return super._msgData(); + 100 : : } + 101 : : + 102 : : /** + 103 : : * @notice Returns the length of the context suffix appended by the forwarder. + 104 : : * @return The context suffix length in bytes. + 105 : : */ + 106 : 828 : function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { + 107 : 828 : return super._contextSuffixLength(); + 108 : : } + 109 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol.func-sort-c.html index 3de5344..a897443 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 9 - 9 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 6 - 6 + 8 + 8 100.0 % @@ -69,28 +69,36 @@ Hit count Sort by hit count - RuleWhitelistOwnable2Step._msgData + RuleWhitelistOwnable2Step._msgData 1 - RuleWhitelistOwnable2Step._authorizeAddressListAdd + RuleWhitelistOwnable2Step._authorizeAddressListAdd 2 - RuleWhitelistOwnable2Step._authorizeAddressListRemove + RuleWhitelistOwnable2Step._authorizeAddressListRemove 2 - RuleWhitelistOwnable2Step._authorizeCheckSpenderManager + RuleWhitelistOwnable2Step._authorizeCheckSpenderManager 2 - RuleWhitelistOwnable2Step._msgSender - 15 + RuleWhitelistOwnable2Step._authorizeMintBurnManager + 3 - RuleWhitelistOwnable2Step._contextSuffixLength - 16 + RuleWhitelistOwnable2Step.supportsInterface + 6 + + + RuleWhitelistOwnable2Step._msgSender + 19 + + + RuleWhitelistOwnable2Step._contextSuffixLength + 20
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol.func.html index 0abcde6..dff580c 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 9 - 9 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 6 - 6 + 8 + 8 100.0 % @@ -69,28 +69,36 @@ Hit count Sort by hit count - RuleWhitelistOwnable2Step._authorizeAddressListAdd + RuleWhitelistOwnable2Step._authorizeAddressListAdd 2 - RuleWhitelistOwnable2Step._authorizeAddressListRemove + RuleWhitelistOwnable2Step._authorizeAddressListRemove 2 - RuleWhitelistOwnable2Step._authorizeCheckSpenderManager + RuleWhitelistOwnable2Step._authorizeCheckSpenderManager 2 - RuleWhitelistOwnable2Step._contextSuffixLength - 16 + RuleWhitelistOwnable2Step._authorizeMintBurnManager + 3 - RuleWhitelistOwnable2Step._msgData + RuleWhitelistOwnable2Step._contextSuffixLength + 20 + + + RuleWhitelistOwnable2Step._msgData 1 - RuleWhitelistOwnable2Step._msgSender - 15 + RuleWhitelistOwnable2Step._msgSender + 19 + + + RuleWhitelistOwnable2Step.supportsInterface + 6
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol.gcov.html index 8d12ab7..c8655d7 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 9 - 9 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 6 - 6 + 8 + 8 100.0 % @@ -75,55 +75,102 @@ 4 : : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 5 : : import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; 6 : : import {Context} from "@openzeppelin/contracts/utils/Context.sol"; - 7 : : import {RuleWhitelistBase} from "../abstract/base/RuleWhitelistBase.sol"; - 8 : : import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; - 9 : : - 10 : : /** - 11 : : * @title RuleWhitelistOwnable2Step - 12 : : * @notice Ownable2Step variant of RuleWhitelist with owner-based authorization hooks. - 13 : : */ - 14 : : contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step { - 15 : : /*////////////////////////////////////////////////////////////// - 16 : : CONSTRUCTOR - 17 : : //////////////////////////////////////////////////////////////*/ - 18 : : - 19 : : /** - 20 : : * @param owner Contract owner. - 21 : : * @param forwarderIrrevocable Address of the ERC-2771 forwarder. - 22 : : * @param checkSpender_ Enables spender checks for transferFrom when true. - 23 : : * @param allowMintBurn Pre-lists `address(0)` at deployment when true. - 24 : : */ - 25 : : constructor(address owner, address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) - 26 : : RuleWhitelistBase(forwarderIrrevocable, checkSpender_, allowMintBurn) - 27 : : Ownable(owner) - 28 : : {} - 29 : : - 30 : : /*////////////////////////////////////////////////////////////// - 31 : : ACCESS CONTROL - 32 : : //////////////////////////////////////////////////////////////*/ - 33 : : - 34 : 2 : function _authorizeAddressListAdd() internal view virtual override onlyOwner {} - 35 : : - 36 : 2 : function _authorizeAddressListRemove() internal view virtual override onlyOwner {} - 37 : : - 38 : 2 : function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} - 39 : : - 40 : : /*////////////////////////////////////////////////////////////// - 41 : : INTERNAL FUNCTIONS - 42 : : //////////////////////////////////////////////////////////////*/ - 43 : : - 44 : 15 : function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { - 45 : 15 : return super._msgSender(); - 46 : : } - 47 : : - 48 : 1 : function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { - 49 : 1 : return super._msgData(); - 50 : : } - 51 : : - 52 : 16 : function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { - 53 : 16 : return super._contextSuffixLength(); - 54 : : } - 55 : : } + 7 : : import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; + 8 : : import {RuleWhitelistBase} from "../abstract/base/RuleWhitelistBase.sol"; + 9 : : import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; + 10 : : + 11 : : /** + 12 : : * @title RuleWhitelistOwnable2Step + 13 : : * @notice Ownable2Step variant of RuleWhitelist with owner-based authorization hooks. + 14 : : */ + 15 : : contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step, Ownable2StepERC165Module { + 16 : : /*////////////////////////////////////////////////////////////// + 17 : : CONSTRUCTOR + 18 : : //////////////////////////////////////////////////////////////*/ + 19 : : + 20 : : /** + 21 : : * @param owner Contract owner. + 22 : : * @param forwarderIrrevocable Address of the ERC-2771 forwarder. + 23 : : * @param checkSpender_ Enables spender checks for transferFrom when true. + 24 : : * @param allowMintBurn Pre-lists `address(0)` at deployment when true. + 25 : : */ + 26 : : constructor(address owner, address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) + 27 : : RuleWhitelistBase(forwarderIrrevocable, checkSpender_, allowMintBurn) + 28 : : Ownable(owner) + 29 : : {} + 30 : : + 31 : : /*////////////////////////////////////////////////////////////// + 32 : : PUBLIC FUNCTIONS + 33 : : //////////////////////////////////////////////////////////////*/ + 34 : : + 35 : : /** + 36 : : * @notice Indicates whether this contract supports a given interface. + 37 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 38 : : * @return True if the interface is supported. + 39 : : */ + 40 : 6 : function supportsInterface(bytes4 interfaceId) + 41 : : public + 42 : : view + 43 : : virtual + 44 : : override(RuleWhitelistBase, Ownable2StepERC165Module) + 45 : : returns (bool) + 46 : : { + 47 : 6 : return Ownable2StepERC165Module.supportsInterface(interfaceId) + 48 : 3 : || RuleWhitelistBase.supportsInterface(interfaceId); + 49 : : } + 50 : : + 51 : : /*////////////////////////////////////////////////////////////// + 52 : : ACCESS CONTROL + 53 : : //////////////////////////////////////////////////////////////*/ + 54 : : + 55 : : /** + 56 : : * @notice Restricts adding addresses to the whitelist to the contract owner. + 57 : : */ + 58 : 2 : function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + 59 : : + 60 : : /** + 61 : : * @notice Restricts removing addresses from the whitelist to the contract owner. + 62 : : */ + 63 : 2 : function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + 64 : : + 65 : : /** + 66 : : * @notice Restricts toggling the spender-check setting to the contract owner. + 67 : : */ + 68 : 2 : function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} + 69 : : + 70 : : /** + 71 : : * @notice Restricts toggling `allowMint` / `allowBurn` to the contract owner. + 72 : : */ + 73 : 3 : function _authorizeMintBurnManager() internal view virtual override onlyOwner {} + 74 : : + 75 : : /*////////////////////////////////////////////////////////////// + 76 : : INTERNAL FUNCTIONS + 77 : : //////////////////////////////////////////////////////////////*/ + 78 : : + 79 : : /** + 80 : : * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + 81 : : * @return sender The address of the message sender. + 82 : : */ + 83 : 19 : function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { + 84 : 19 : return super._msgSender(); + 85 : : } + 86 : : + 87 : : /** + 88 : : * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + 89 : : * @return The message calldata. + 90 : : */ + 91 : 1 : function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { + 92 : 1 : return super._msgData(); + 93 : : } + 94 : : + 95 : : /** + 96 : : * @notice Returns the length of the context suffix appended by the forwarder. + 97 : : * @return The context suffix length in bytes. + 98 : : */ + 99 : 20 : function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { + 100 : 20 : return super._contextSuffixLength(); + 101 : : } + 102 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapper.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapper.sol.func-sort-c.html index 8b9413d..fd0b1f5 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapper.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapper.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 17 - 17 + 19 + 19 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 9 - 9 + 11 + 11 100.0 % @@ -69,40 +69,48 @@ Hit count Sort by hit count - RuleWhitelistWrapper._msgData + RuleWhitelistWrapper._msgData 1 - RuleWhitelistWrapper._revokeRole + RuleWhitelistWrapper._revokeRole 1 - RuleWhitelistWrapper._authorizeCheckSpenderManager + RuleWhitelistWrapper._authorizeCheckSpenderManager 2 - RuleWhitelistWrapper._grantRole - 38 + RuleWhitelistWrapper._onlyRulesLimitManager + 2 + + + RuleWhitelistWrapper._authorizeMintBurnManager + 4 + + + RuleWhitelistWrapper.supportsInterface + 47 - RuleWhitelistWrapper.hasRole - 38 + RuleWhitelistWrapper._grantRole + 49 - RuleWhitelistWrapper.supportsInterface - 46 + RuleWhitelistWrapper.hasRole + 49 - RuleWhitelistWrapper._onlyRulesManager - 90 + RuleWhitelistWrapper._onlyRulesManager + 98 - RuleWhitelistWrapper._msgSender - 133 + RuleWhitelistWrapper._msgSender + 158 - RuleWhitelistWrapper._contextSuffixLength - 134 + RuleWhitelistWrapper._contextSuffixLength + 159
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapper.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapper.sol.func.html index 80fbc74..9da6f75 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapper.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapper.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 17 - 17 + 19 + 19 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 9 - 9 + 11 + 11 100.0 % @@ -69,40 +69,48 @@ Hit count Sort by hit count - RuleWhitelistWrapper._authorizeCheckSpenderManager + RuleWhitelistWrapper._authorizeCheckSpenderManager 2 - RuleWhitelistWrapper._contextSuffixLength - 134 + RuleWhitelistWrapper._authorizeMintBurnManager + 4 - RuleWhitelistWrapper._grantRole - 38 + RuleWhitelistWrapper._contextSuffixLength + 159 - RuleWhitelistWrapper._msgData + RuleWhitelistWrapper._grantRole + 49 + + + RuleWhitelistWrapper._msgData 1 - RuleWhitelistWrapper._msgSender - 133 + RuleWhitelistWrapper._msgSender + 158 + + + RuleWhitelistWrapper._onlyRulesLimitManager + 2 - RuleWhitelistWrapper._onlyRulesManager - 90 + RuleWhitelistWrapper._onlyRulesManager + 98 - RuleWhitelistWrapper._revokeRole + RuleWhitelistWrapper._revokeRole 1 - RuleWhitelistWrapper.hasRole - 38 + RuleWhitelistWrapper.hasRole + 49 - RuleWhitelistWrapper.supportsInterface - 46 + RuleWhitelistWrapper.supportsInterface + 47
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapper.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapper.sol.gcov.html index 94f7837..c3d807a 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapper.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapper.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 17 - 17 + 19 + 19 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 9 - 9 + 11 + 11 100.0 % @@ -79,102 +79,141 @@ 8 : : /* ==== Abstract contracts === */ 9 : : import {AccessControlModuleStandalone} from "../../../modules/AccessControlModuleStandalone.sol"; 10 : : import {RuleWhitelistWrapperBase} from "../abstract/base/RuleWhitelistWrapperBase.sol"; - 11 : : - 12 : : /** - 13 : : * @title Wrapper to call several different whitelist rules - 14 : : */ - 15 : : contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleStandalone { - 16 : : /*////////////////////////////////////////////////////////////// - 17 : : CONSTRUCTOR - 18 : : //////////////////////////////////////////////////////////////*/ - 19 : : /** - 20 : : * @param admin Address of the contract (Access Control) - 21 : : * @param forwarderIrrevocable Address of the forwarder, required for the gasless support - 22 : : */ - 23 : : constructor(address admin, address forwarderIrrevocable, bool checkSpender_) - 24 : : RuleWhitelistWrapperBase(forwarderIrrevocable, checkSpender_) - 25 : : AccessControlModuleStandalone(admin) - 26 : : {} - 27 : : - 28 : : /*////////////////////////////////////////////////////////////// - 29 : : PUBLIC FUNCTIONS - 30 : : //////////////////////////////////////////////////////////////*/ - 31 : : - 32 : : /** - 33 : : * @dev Returns `true` if `account` has been granted `role`. - 34 : : */ - 35 : 38 : function hasRole(bytes32 role, address account) - 36 : : public - 37 : : view - 38 : : virtual - 39 : : override - 40 : : returns (bool) - 41 : : { - 42 : 134 : return AccessControlModuleStandalone.hasRole(role, account); - 43 : : } - 44 : : - 45 : 46 : function supportsInterface(bytes4 interfaceId) - 46 : : public - 47 : : view - 48 : : virtual - 49 : : override(AccessControlEnumerable, RuleWhitelistWrapperBase) - 50 : : returns (bool) - 51 : : { - 52 : 46 : return RuleWhitelistWrapperBase.supportsInterface(interfaceId) - 53 : 31 : || AccessControlEnumerable.supportsInterface(interfaceId); - 54 : : } - 55 : : - 56 : : /*////////////////////////////////////////////////////////////// - 57 : : ACCESS CONTROL - 58 : : //////////////////////////////////////////////////////////////*/ - 59 : : - 60 : 2 : function _authorizeCheckSpenderManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} - 61 : : - 62 : : /** - 63 : : * @dev Restrict rules management to the dedicated role. - 64 : : */ - 65 : 90 : function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + 11 : : /* ==== RuleEngine === */ + 12 : : import {RulesManagementModuleRolesStorage} from "RuleEngine/modules/library/RulesManagementModuleRolesStorage.sol"; + 13 : : + 14 : : /** + 15 : : * @title Wrapper to call several different whitelist rules + 16 : : */ + 17 : : contract RuleWhitelistWrapper is + 18 : : RuleWhitelistWrapperBase, + 19 : : AccessControlModuleStandalone, + 20 : : RulesManagementModuleRolesStorage + 21 : : { + 22 : : /*////////////////////////////////////////////////////////////// + 23 : : CONSTRUCTOR + 24 : : //////////////////////////////////////////////////////////////*/ + 25 : : /** + 26 : : * @param admin Address of the contract (Access Control) + 27 : : * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + 28 : : * @param checkSpender_ Enables spender checks for transferFrom when true. + 29 : : * @param allowMintBurn When true, permits both minting and burning (sets `allowMint` and `allowBurn`). + 30 : : */ + 31 : : constructor(address admin, address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) + 32 : : RuleWhitelistWrapperBase(forwarderIrrevocable, checkSpender_, allowMintBurn) + 33 : : AccessControlModuleStandalone(admin) + 34 : : {} + 35 : : + 36 : : /*////////////////////////////////////////////////////////////// + 37 : : PUBLIC FUNCTIONS + 38 : : //////////////////////////////////////////////////////////////*/ + 39 : : + 40 : : /** + 41 : : * @notice Returns whether `account` has been granted `role`. + 42 : : * @dev Returns `true` if `account` has been granted `role`. + 43 : : * @param role Role identifier being queried. + 44 : : * @param account Address being checked for the role. + 45 : : * @return True if `account` holds `role`. + 46 : : */ + 47 : 49 : function hasRole(bytes32 role, address account) public view virtual override returns (bool) { + 48 : 159 : return AccessControlModuleStandalone.hasRole(role, account); + 49 : : } + 50 : : + 51 : : /** + 52 : : * @notice Indicates whether this contract supports a given interface. + 53 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 54 : : * @return True if the interface is supported. + 55 : : */ + 56 : 47 : function supportsInterface(bytes4 interfaceId) + 57 : : public + 58 : : view + 59 : : virtual + 60 : : override(AccessControlEnumerable, RuleWhitelistWrapperBase) + 61 : : returns (bool) + 62 : : { + 63 : 47 : return RuleWhitelistWrapperBase.supportsInterface(interfaceId) + 64 : 32 : || AccessControlEnumerable.supportsInterface(interfaceId); + 65 : : } 66 : : 67 : : /*////////////////////////////////////////////////////////////// - 68 : : INTERNAL FUNCTIONS + 68 : : ACCESS CONTROL 69 : : //////////////////////////////////////////////////////////////*/ 70 : : - 71 : 133 : function _msgSender() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (address sender) { - 72 : 133 : return RuleWhitelistWrapperBase._msgSender(); - 73 : : } - 74 : : - 75 : 1 : function _msgData() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (bytes calldata) { - 76 : 1 : return RuleWhitelistWrapperBase._msgData(); - 77 : : } - 78 : : - 79 : 134 : function _contextSuffixLength() - 80 : : internal - 81 : : view - 82 : : virtual - 83 : : override(RuleWhitelistWrapperBase, Context) - 84 : : returns (uint256) - 85 : : { - 86 : 134 : return RuleWhitelistWrapperBase._contextSuffixLength(); - 87 : : } - 88 : : - 89 : 38 : function _grantRole(bytes32 role, address account) - 90 : : internal - 91 : : virtual - 92 : : override - 93 : : returns (bool) - 94 : : { - 95 : 38 : return AccessControlEnumerable._grantRole(role, account); - 96 : : } - 97 : : - 98 : 1 : function _revokeRole(bytes32 role, address account) - 99 : : internal - 100 : : virtual - 101 : : override - 102 : : returns (bool) - 103 : : { - 104 : 1 : return AccessControlEnumerable._revokeRole(role, account); - 105 : : } - 106 : : } + 71 : : /** + 72 : : * @notice Restricts toggling the spender-check setting to holders of DEFAULT_ADMIN_ROLE. + 73 : : */ + 74 : 2 : function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + 75 : : + 76 : : /** + 77 : : * @notice Restricts toggling `allowMint` / `allowBurn` to holders of DEFAULT_ADMIN_ROLE. + 78 : : */ + 79 : 4 : function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + 80 : : + 81 : : /** + 82 : : * @notice Restricts rules management to holders of RULES_MANAGEMENT_ROLE. + 83 : : * @dev Restrict rules management to the dedicated role. + 84 : : */ + 85 : 98 : function _onlyRulesManager() internal view virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + 86 : : + 87 : : /** + 88 : : * @notice Restricts rules-limit management to holders of RULES_MANAGEMENT_ROLE. + 89 : : */ + 90 : 2 : function _onlyRulesLimitManager() internal view virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + 91 : : + 92 : : /*////////////////////////////////////////////////////////////// + 93 : : INTERNAL FUNCTIONS + 94 : : //////////////////////////////////////////////////////////////*/ + 95 : : + 96 : : /** + 97 : : * @notice Grants `role` to `account`, keeping role enumeration in sync. + 98 : : * @param role Role identifier to grant. + 99 : : * @param account Address receiving the role. + 100 : : * @return True if the role was newly granted. + 101 : : */ + 102 : 49 : function _grantRole(bytes32 role, address account) internal virtual override returns (bool) { + 103 : 49 : return AccessControlEnumerable._grantRole(role, account); + 104 : : } + 105 : : + 106 : : /** + 107 : : * @notice Revokes `role` from `account`, keeping role enumeration in sync. + 108 : : * @param role Role identifier to revoke. + 109 : : * @param account Address losing the role. + 110 : : * @return True if the role was previously held and is now revoked. + 111 : : */ + 112 : 1 : function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) { + 113 : 1 : return AccessControlEnumerable._revokeRole(role, account); + 114 : : } + 115 : : + 116 : : /** + 117 : : * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + 118 : : * @return sender The address of the message sender. + 119 : : */ + 120 : 158 : function _msgSender() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (address sender) { + 121 : 158 : return RuleWhitelistWrapperBase._msgSender(); + 122 : : } + 123 : : + 124 : : /** + 125 : : * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + 126 : : * @return The message calldata. + 127 : : */ + 128 : 1 : function _msgData() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (bytes calldata) { + 129 : 1 : return RuleWhitelistWrapperBase._msgData(); + 130 : : } + 131 : : + 132 : : /** + 133 : : * @notice Returns the length of the context suffix appended by the forwarder. + 134 : : * @return The context suffix length in bytes. + 135 : : */ + 136 : 159 : function _contextSuffixLength() + 137 : : internal + 138 : : view + 139 : : virtual + 140 : : override(RuleWhitelistWrapperBase, Context) + 141 : : returns (uint256) + 142 : : { + 143 : 159 : return RuleWhitelistWrapperBase._contextSuffixLength(); + 144 : : } + 145 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol.func-sort-c.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol.func-sort-c.html index c89b317..4a428f9 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol.func-sort-c.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol.func-sort-c.html @@ -31,17 +31,17 @@ lcov.info Lines: - 8 - 8 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 5 - 5 + 8 + 8 100.0 % @@ -69,24 +69,36 @@ Hit count Sort by hit count - RuleWhitelistWrapperOwnable2Step._msgData + RuleWhitelistWrapperOwnable2Step._msgData 1 - RuleWhitelistWrapperOwnable2Step._authorizeCheckSpenderManager + RuleWhitelistWrapperOwnable2Step._onlyRulesLimitManager + 1 + + + RuleWhitelistWrapperOwnable2Step._authorizeCheckSpenderManager 2 - RuleWhitelistWrapperOwnable2Step._onlyRulesManager + RuleWhitelistWrapperOwnable2Step._onlyRulesManager 2 - RuleWhitelistWrapperOwnable2Step._msgSender - 12 + RuleWhitelistWrapperOwnable2Step._authorizeMintBurnManager + 3 + + + RuleWhitelistWrapperOwnable2Step.supportsInterface + 5 + + + RuleWhitelistWrapperOwnable2Step._msgSender + 17 - RuleWhitelistWrapperOwnable2Step._contextSuffixLength - 13 + RuleWhitelistWrapperOwnable2Step._contextSuffixLength + 18
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol.func.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol.func.html index b09287f..3f876ea 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol.func.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol.func.html @@ -31,17 +31,17 @@ lcov.info Lines: - 8 - 8 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 5 - 5 + 8 + 8 100.0 % @@ -69,25 +69,37 @@ Hit count Sort by hit count - RuleWhitelistWrapperOwnable2Step._authorizeCheckSpenderManager + RuleWhitelistWrapperOwnable2Step._authorizeCheckSpenderManager 2 - RuleWhitelistWrapperOwnable2Step._contextSuffixLength - 13 + RuleWhitelistWrapperOwnable2Step._authorizeMintBurnManager + 3 + + + RuleWhitelistWrapperOwnable2Step._contextSuffixLength + 18 - RuleWhitelistWrapperOwnable2Step._msgData + RuleWhitelistWrapperOwnable2Step._msgData 1 - RuleWhitelistWrapperOwnable2Step._msgSender - 12 + RuleWhitelistWrapperOwnable2Step._msgSender + 17 - RuleWhitelistWrapperOwnable2Step._onlyRulesManager + RuleWhitelistWrapperOwnable2Step._onlyRulesLimitManager + 1 + + + RuleWhitelistWrapperOwnable2Step._onlyRulesManager 2 + + RuleWhitelistWrapperOwnable2Step.supportsInterface + 5 +
diff --git a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol.gcov.html b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol.gcov.html index 726dff2..7bca084 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol.gcov.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol.gcov.html @@ -31,17 +31,17 @@ lcov.info Lines: - 8 - 8 + 13 + 13 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 5 - 5 + 8 + 8 100.0 % @@ -77,58 +77,107 @@ 6 : : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 7 : : import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; 8 : : import {Context} from "@openzeppelin/contracts/utils/Context.sol"; - 9 : : /* ==== Abstract contracts === */ - 10 : : import {RuleWhitelistWrapperBase} from "../abstract/base/RuleWhitelistWrapperBase.sol"; - 11 : : - 12 : : /** - 13 : : * @title Wrapper to call several different whitelist rules (Ownable2Step) - 14 : : */ - 15 : : contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2Step { - 16 : : /*////////////////////////////////////////////////////////////// - 17 : : CONSTRUCTOR - 18 : : //////////////////////////////////////////////////////////////*/ - 19 : : /** - 20 : : * @param owner Address of the contract owner - 21 : : * @param forwarderIrrevocable Address of the forwarder, required for the gasless support - 22 : : */ - 23 : : constructor(address owner, address forwarderIrrevocable, bool checkSpender_) - 24 : : RuleWhitelistWrapperBase(forwarderIrrevocable, checkSpender_) - 25 : : Ownable(owner) - 26 : : {} - 27 : : - 28 : : /*////////////////////////////////////////////////////////////// - 29 : : ACCESS CONTROL - 30 : : //////////////////////////////////////////////////////////////*/ - 31 : : - 32 : 2 : function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} - 33 : : - 34 : : /** - 35 : : * @dev Restrict rules management to the owner. - 36 : : */ - 37 : 2 : function _onlyRulesManager() internal view virtual override onlyOwner {} - 38 : : - 39 : : /*////////////////////////////////////////////////////////////// - 40 : : INTERNAL FUNCTIONS - 41 : : //////////////////////////////////////////////////////////////*/ - 42 : : - 43 : 12 : function _msgSender() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (address sender) { - 44 : 12 : return RuleWhitelistWrapperBase._msgSender(); - 45 : : } - 46 : : - 47 : 1 : function _msgData() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (bytes calldata) { - 48 : 1 : return RuleWhitelistWrapperBase._msgData(); + 9 : : import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; + 10 : : /* ==== Abstract contracts === */ + 11 : : import {RuleWhitelistWrapperBase} from "../abstract/base/RuleWhitelistWrapperBase.sol"; + 12 : : + 13 : : /** + 14 : : * @title Wrapper to call several different whitelist rules (Ownable2Step) + 15 : : */ + 16 : : contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2Step, Ownable2StepERC165Module { + 17 : : /*////////////////////////////////////////////////////////////// + 18 : : CONSTRUCTOR + 19 : : //////////////////////////////////////////////////////////////*/ + 20 : : /** + 21 : : * @param owner Address of the contract owner + 22 : : * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + 23 : : * @param checkSpender_ Enables spender checks for transferFrom when true. + 24 : : * @param allowMintBurn When true, permits both minting and burning (sets `allowMint` and `allowBurn`). + 25 : : */ + 26 : : constructor(address owner, address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) + 27 : : RuleWhitelistWrapperBase(forwarderIrrevocable, checkSpender_, allowMintBurn) + 28 : : Ownable(owner) + 29 : : {} + 30 : : + 31 : : /*////////////////////////////////////////////////////////////// + 32 : : PUBLIC FUNCTIONS + 33 : : //////////////////////////////////////////////////////////////*/ + 34 : : + 35 : : /** + 36 : : * @notice Indicates whether this contract supports a given interface. + 37 : : * @param interfaceId The interface identifier, as specified in ERC-165. + 38 : : * @return True if the interface is supported. + 39 : : */ + 40 : 5 : function supportsInterface(bytes4 interfaceId) + 41 : : public + 42 : : view + 43 : : virtual + 44 : : override(RuleWhitelistWrapperBase, Ownable2StepERC165Module) + 45 : : returns (bool) + 46 : : { + 47 : 5 : return Ownable2StepERC165Module.supportsInterface(interfaceId) + 48 : 2 : || RuleWhitelistWrapperBase.supportsInterface(interfaceId); 49 : : } 50 : : - 51 : 13 : function _contextSuffixLength() - 52 : : internal - 53 : : view - 54 : : virtual - 55 : : override(RuleWhitelistWrapperBase, Context) - 56 : : returns (uint256) - 57 : : { - 58 : 13 : return RuleWhitelistWrapperBase._contextSuffixLength(); - 59 : : } - 60 : : } + 51 : : /*////////////////////////////////////////////////////////////// + 52 : : ACCESS CONTROL + 53 : : //////////////////////////////////////////////////////////////*/ + 54 : : + 55 : : /** + 56 : : * @notice Restricts toggling the spender-check setting to the contract owner. + 57 : : */ + 58 : 2 : function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} + 59 : : + 60 : : /** + 61 : : * @notice Restricts toggling `allowMint` / `allowBurn` to the contract owner. + 62 : : */ + 63 : 3 : function _authorizeMintBurnManager() internal view virtual override onlyOwner {} + 64 : : + 65 : : /** + 66 : : * @notice Restricts rules management to the contract owner. + 67 : : * @dev Restrict rules management to the owner. + 68 : : */ + 69 : 2 : function _onlyRulesManager() internal view virtual override onlyOwner {} + 70 : : + 71 : : /** + 72 : : * @notice Restricts rules-limit management to the contract owner. + 73 : : */ + 74 : 1 : function _onlyRulesLimitManager() internal view virtual override onlyOwner {} + 75 : : + 76 : : /*////////////////////////////////////////////////////////////// + 77 : : INTERNAL FUNCTIONS + 78 : : //////////////////////////////////////////////////////////////*/ + 79 : : + 80 : : /** + 81 : : * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + 82 : : * @return sender The address of the message sender. + 83 : : */ + 84 : 17 : function _msgSender() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (address sender) { + 85 : 17 : return RuleWhitelistWrapperBase._msgSender(); + 86 : : } + 87 : : + 88 : : /** + 89 : : * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + 90 : : * @return The message calldata. + 91 : : */ + 92 : 1 : function _msgData() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (bytes calldata) { + 93 : 1 : return RuleWhitelistWrapperBase._msgData(); + 94 : : } + 95 : : + 96 : : /** + 97 : : * @notice Returns the length of the context suffix appended by the forwarder. + 98 : : * @return The context suffix length in bytes. + 99 : : */ + 100 : 18 : function _contextSuffixLength() + 101 : : internal + 102 : : view + 103 : : virtual + 104 : : override(RuleWhitelistWrapperBase, Context) + 105 : : returns (uint256) + 106 : : { + 107 : 18 : return RuleWhitelistWrapperBase._contextSuffixLength(); + 108 : : } + 109 : : } diff --git a/doc/coverage/coverage/src/rules/validation/deployment/index-sort-b.html b/doc/coverage/coverage/src/rules/validation/deployment/index-sort-b.html index e57f953..b927563 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/index-sort-b.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/index-sort-b.html @@ -31,17 +31,17 @@ lcov.info Lines: - 133 - 133 + 164 + 164 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 79 - 79 + 95 + 95 100.0 % @@ -82,91 +82,91 @@ Branches Sort by branch coverage - RuleMaxTotalSupply.sol + RuleWhitelistWrapperOwnable2Step.sol
100.0%
100.0 % - 4 / 4 + 13 / 13 100.0 % - 2 / 2 + 8 / 8 - 0 / 0 - RuleSpenderWhitelistOwnable2Step.sol + RuleSanctionsList.sol
100.0%
100.0 % - 8 / 8 + 10 / 10 100.0 % 5 / 5 - 0 / 0 - RuleERC2980Ownable2Step.sol + RuleMaxTotalSupply.sol
100.0%
100.0 % - 10 / 10 + 4 / 4 100.0 % - 7 / 7 + 2 / 2 - 0 / 0 - RuleWhitelistOwnable2Step.sol + RuleERC2980.sol
100.0%
100.0 % - 9 / 9 + 13 / 13 100.0 % - 6 / 6 + 9 / 9 - 0 / 0 - RuleBlacklist.sol + RuleWhitelist.sol
100.0%
100.0 % - 11 / 11 + 13 / 13 100.0 % - 6 / 6 + 8 / 8 - 0 / 0 - RuleWhitelist.sol + RuleWhitelistOwnable2Step.sol
100.0%
100.0 % - 12 / 12 + 13 / 13 100.0 % - 7 / 7 + 8 / 8 - 0 / 0 - RuleSanctionsList.sol + RuleIdentityRegistryOwnable2Step.sol
100.0%
100.0 % - 10 / 10 + 4 / 4 100.0 % - 5 / 5 + 2 / 2 - 0 / 0 - RuleSpenderWhitelist.sol + RuleBlacklist.sol
100.0%
@@ -178,98 +178,98 @@ 0 / 0 - RuleIdentityRegistry.sol + RuleSanctionsListOwnable2Step.sol
100.0%
100.0 % - 4 / 4 + 10 / 10 100.0 % - 2 / 2 + 5 / 5 - 0 / 0 - RuleSanctionsListOwnable2Step.sol + RuleIdentityRegistry.sol
100.0%
100.0 % - 7 / 7 - 100.0 % 4 / 4 + 100.0 % + 2 / 2 - 0 / 0 - RuleBlacklistOwnable2Step.sol + RuleSpenderWhitelistOwnable2Step.sol
100.0%
100.0 % - 8 / 8 + 11 / 11 100.0 % - 5 / 5 + 6 / 6 - 0 / 0 - RuleWhitelistWrapper.sol + RuleERC2980Ownable2Step.sol
100.0%
100.0 % - 17 / 17 + 13 / 13 100.0 % 9 / 9 - 0 / 0 - RuleERC2980.sol + RuleMaxTotalSupplyOwnable2Step.sol
100.0%
100.0 % - 12 / 12 + 4 / 4 100.0 % - 8 / 8 + 2 / 2 - 0 / 0 - RuleMaxTotalSupplyOwnable2Step.sol + RuleWhitelistWrapper.sol
100.0%
100.0 % - 1 / 1 + 19 / 19 100.0 % - 1 / 1 + 11 / 11 - 0 / 0 - RuleIdentityRegistryOwnable2Step.sol + RuleSpenderWhitelist.sol
100.0%
100.0 % - 1 / 1 + 11 / 11 100.0 % - 1 / 1 + 6 / 6 - 0 / 0 - RuleWhitelistWrapperOwnable2Step.sol + RuleBlacklistOwnable2Step.sol
100.0%
100.0 % - 8 / 8 + 11 / 11 100.0 % - 5 / 5 + 6 / 6 - 0 / 0 diff --git a/doc/coverage/coverage/src/rules/validation/deployment/index-sort-f.html b/doc/coverage/coverage/src/rules/validation/deployment/index-sort-f.html index a62cbc5..147d780 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/index-sort-f.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/index-sort-f.html @@ -31,17 +31,17 @@ lcov.info Lines: - 133 - 133 + 164 + 164 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 79 - 79 + 95 + 95 100.0 % @@ -82,14 +82,14 @@ Branches Sort by branch coverage - RuleMaxTotalSupplyOwnable2Step.sol + RuleMaxTotalSupply.sol
100.0%
100.0 % - 1 / 1 + 4 / 4 100.0 % - 1 / 1 + 2 / 2 - 0 / 0 @@ -99,14 +99,14 @@
100.0%
100.0 % - 1 / 1 + 4 / 4 100.0 % - 1 / 1 + 2 / 2 - 0 / 0 - RuleMaxTotalSupply.sol + RuleIdentityRegistry.sol
100.0%
@@ -118,7 +118,7 @@ 0 / 0 - RuleIdentityRegistry.sol + RuleMaxTotalSupplyOwnable2Step.sol
100.0%
@@ -130,134 +130,134 @@ 0 / 0 - RuleSanctionsListOwnable2Step.sol + RuleSanctionsList.sol
100.0%
100.0 % - 7 / 7 + 10 / 10 100.0 % - 4 / 4 + 5 / 5 - 0 / 0 - RuleSpenderWhitelistOwnable2Step.sol + RuleSanctionsListOwnable2Step.sol
100.0%
100.0 % - 8 / 8 + 10 / 10 100.0 % 5 / 5 - 0 / 0 - RuleSanctionsList.sol + RuleBlacklist.sol
100.0%
100.0 % - 10 / 10 + 11 / 11 100.0 % - 5 / 5 + 6 / 6 - 0 / 0 - RuleBlacklistOwnable2Step.sol + RuleSpenderWhitelistOwnable2Step.sol
100.0%
100.0 % - 8 / 8 + 11 / 11 100.0 % - 5 / 5 + 6 / 6 - 0 / 0 - RuleWhitelistWrapperOwnable2Step.sol + RuleSpenderWhitelist.sol
100.0%
100.0 % - 8 / 8 + 11 / 11 100.0 % - 5 / 5 + 6 / 6 - 0 / 0 - RuleWhitelistOwnable2Step.sol + RuleBlacklistOwnable2Step.sol
100.0%
100.0 % - 9 / 9 + 11 / 11 100.0 % 6 / 6 - 0 / 0 - RuleBlacklist.sol + RuleWhitelistWrapperOwnable2Step.sol
100.0%
100.0 % - 11 / 11 + 13 / 13 100.0 % - 6 / 6 + 8 / 8 - 0 / 0 - RuleSpenderWhitelist.sol + RuleWhitelist.sol
100.0%
100.0 % - 11 / 11 + 13 / 13 100.0 % - 6 / 6 + 8 / 8 - 0 / 0 - RuleERC2980Ownable2Step.sol + RuleWhitelistOwnable2Step.sol
100.0%
100.0 % - 10 / 10 + 13 / 13 100.0 % - 7 / 7 + 8 / 8 - 0 / 0 - RuleWhitelist.sol + RuleERC2980.sol
100.0%
100.0 % - 12 / 12 + 13 / 13 100.0 % - 7 / 7 + 9 / 9 - 0 / 0 - RuleERC2980.sol + RuleERC2980Ownable2Step.sol
100.0%
100.0 % - 12 / 12 + 13 / 13 100.0 % - 8 / 8 + 9 / 9 - 0 / 0 @@ -267,9 +267,9 @@
100.0%
100.0 % - 17 / 17 + 19 / 19 100.0 % - 9 / 9 + 11 / 11 - 0 / 0 diff --git a/doc/coverage/coverage/src/rules/validation/deployment/index-sort-l.html b/doc/coverage/coverage/src/rules/validation/deployment/index-sort-l.html index a65138a..fb11735 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/index-sort-l.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/index-sort-l.html @@ -31,17 +31,17 @@ lcov.info Lines: - 133 - 133 + 164 + 164 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 79 - 79 + 95 + 95 100.0 % @@ -82,14 +82,14 @@ Branches Sort by branch coverage - RuleMaxTotalSupplyOwnable2Step.sol + RuleMaxTotalSupply.sol
100.0%
100.0 % - 1 / 1 + 4 / 4 100.0 % - 1 / 1 + 2 / 2 - 0 / 0 @@ -99,14 +99,14 @@
100.0%
100.0 % - 1 / 1 + 4 / 4 100.0 % - 1 / 1 + 2 / 2 - 0 / 0 - RuleMaxTotalSupply.sol + RuleIdentityRegistry.sol
100.0%
@@ -118,7 +118,7 @@ 0 / 0 - RuleIdentityRegistry.sol + RuleMaxTotalSupplyOwnable2Step.sol
100.0%
@@ -130,134 +130,134 @@ 0 / 0 - RuleSanctionsListOwnable2Step.sol + RuleSanctionsList.sol
100.0%
100.0 % - 7 / 7 + 10 / 10 100.0 % - 4 / 4 + 5 / 5 - 0 / 0 - RuleSpenderWhitelistOwnable2Step.sol + RuleSanctionsListOwnable2Step.sol
100.0%
100.0 % - 8 / 8 + 10 / 10 100.0 % 5 / 5 - 0 / 0 - RuleBlacklistOwnable2Step.sol + RuleBlacklist.sol
100.0%
100.0 % - 8 / 8 + 11 / 11 100.0 % - 5 / 5 + 6 / 6 - 0 / 0 - RuleWhitelistWrapperOwnable2Step.sol + RuleSpenderWhitelistOwnable2Step.sol
100.0%
100.0 % - 8 / 8 + 11 / 11 100.0 % - 5 / 5 + 6 / 6 - 0 / 0 - RuleWhitelistOwnable2Step.sol + RuleSpenderWhitelist.sol
100.0%
100.0 % - 9 / 9 + 11 / 11 100.0 % 6 / 6 - 0 / 0 - RuleERC2980Ownable2Step.sol + RuleBlacklistOwnable2Step.sol
100.0%
100.0 % - 10 / 10 + 11 / 11 100.0 % - 7 / 7 + 6 / 6 - 0 / 0 - RuleSanctionsList.sol + RuleWhitelistWrapperOwnable2Step.sol
100.0%
100.0 % - 10 / 10 + 13 / 13 100.0 % - 5 / 5 + 8 / 8 - 0 / 0 - RuleBlacklist.sol + RuleERC2980.sol
100.0%
100.0 % - 11 / 11 + 13 / 13 100.0 % - 6 / 6 + 9 / 9 - 0 / 0 - RuleSpenderWhitelist.sol + RuleWhitelist.sol
100.0%
100.0 % - 11 / 11 + 13 / 13 100.0 % - 6 / 6 + 8 / 8 - 0 / 0 - RuleWhitelist.sol + RuleWhitelistOwnable2Step.sol
100.0%
100.0 % - 12 / 12 + 13 / 13 100.0 % - 7 / 7 + 8 / 8 - 0 / 0 - RuleERC2980.sol + RuleERC2980Ownable2Step.sol
100.0%
100.0 % - 12 / 12 + 13 / 13 100.0 % - 8 / 8 + 9 / 9 - 0 / 0 @@ -267,9 +267,9 @@
100.0%
100.0 % - 17 / 17 + 19 / 19 100.0 % - 9 / 9 + 11 / 11 - 0 / 0 diff --git a/doc/coverage/coverage/src/rules/validation/deployment/index.html b/doc/coverage/coverage/src/rules/validation/deployment/index.html index 4a56d2b..eb083c3 100644 --- a/doc/coverage/coverage/src/rules/validation/deployment/index.html +++ b/doc/coverage/coverage/src/rules/validation/deployment/index.html @@ -31,17 +31,17 @@ lcov.info Lines: - 133 - 133 + 164 + 164 100.0 % Date: - 2026-04-16 15:29:51 + 2026-07-14 13:44:06 Functions: - 79 - 79 + 95 + 95 100.0 % @@ -99,9 +99,9 @@
100.0%
100.0 % - 8 / 8 + 11 / 11 100.0 % - 5 / 5 + 6 / 6 - 0 / 0 @@ -111,9 +111,9 @@
100.0%
100.0 % - 12 / 12 + 13 / 13 100.0 % - 8 / 8 + 9 / 9 - 0 / 0 @@ -123,9 +123,9 @@
100.0%
100.0 % - 10 / 10 + 13 / 13 100.0 % - 7 / 7 + 9 / 9 - 0 / 0 @@ -147,9 +147,9 @@
100.0%
100.0 % - 1 / 1 + 4 / 4 100.0 % - 1 / 1 + 2 / 2 - 0 / 0 @@ -171,9 +171,9 @@
100.0%
100.0 % - 1 / 1 + 4 / 4 100.0 % - 1 / 1 + 2 / 2 - 0 / 0 @@ -195,9 +195,9 @@
100.0%
100.0 % - 7 / 7 + 10 / 10 100.0 % - 4 / 4 + 5 / 5 - 0 / 0 @@ -219,9 +219,9 @@
100.0%
100.0 % - 8 / 8 + 11 / 11 100.0 % - 5 / 5 + 6 / 6 - 0 / 0 @@ -231,9 +231,9 @@
100.0%
100.0 % - 12 / 12 + 13 / 13 100.0 % - 7 / 7 + 8 / 8 - 0 / 0 @@ -243,9 +243,9 @@
100.0%
100.0 % - 9 / 9 + 13 / 13 100.0 % - 6 / 6 + 8 / 8 - 0 / 0 @@ -255,9 +255,9 @@
100.0%
100.0 % - 17 / 17 + 19 / 19 100.0 % - 9 / 9 + 11 / 11 - 0 / 0 @@ -267,9 +267,9 @@
100.0%
100.0 % - 8 / 8 + 13 / 13 100.0 % - 5 / 5 + 8 / 8 - 0 / 0 diff --git a/doc/coverage/lcov.info b/doc/coverage/lcov.info index 41f4b88..787ebee 100644 --- a/doc/coverage/lcov.info +++ b/doc/coverage/lcov.info @@ -1,20 +1,20 @@ TN: SF:src/modules/AccessControlModuleStandalone.sol -DA:27,430 -FN:27,AccessControlModuleStandalone.constructor -FNDA:430,AccessControlModuleStandalone.constructor -DA:28,430 -BRDA:28,0,0,6 -BRDA:28,0,1,424 -DA:32,424 -DA:42,540 -FN:42,AccessControlModuleStandalone.hasRole -FNDA:540,AccessControlModuleStandalone.hasRole -DA:51,3627 -BRDA:51,1,0,3104 -BRDA:51,1,1,523 -DA:52,3104 -DA:54,523 +DA:30,1622 +FN:30,AccessControlModuleStandalone.constructor +FNDA:1622,AccessControlModuleStandalone.constructor +DA:31,1622 +BRDA:31,0,0,7 +BRDA:31,0,1,1615 +DA:35,1615 +DA:46,1746 +FN:46,AccessControlModuleStandalone.hasRole +FNDA:1746,AccessControlModuleStandalone.hasRole +DA:55,21064 +BRDA:55,1,0,4032 +BRDA:55,1,1,17032 +DA:56,4032 +DA:58,17032 FNF:2 FNH:2 LF:7 @@ -23,11 +23,26 @@ BRF:4 BRH:4 end_of_record TN: +SF:src/modules/Ownable2StepERC165Module.sol +DA:17,68 +FN:17,Ownable2StepERC165Module.supportsInterface +FNDA:68,Ownable2StepERC165Module.supportsInterface +DA:18,68 +DA:19,57 +DA:20,46 +FNF:1 +FNH:1 +LF:4 +LH:4 +BRF:0 +BRH:0 +end_of_record +TN: SF:src/modules/VersionModule.sol -DA:18,7 -FN:18,VersionModule.version +DA:23,7 +FN:23,VersionModule.version FNDA:7,VersionModule.version -DA:19,7 +DA:24,7 FNF:1 FNH:1 LF:2 @@ -37,887 +52,1384 @@ BRH:0 end_of_record TN: SF:src/rules/operation/RuleConditionalTransferLight.sol -DA:33,21 -FN:33,RuleConditionalTransferLight.supportsInterface -FNDA:21,RuleConditionalTransferLight.supportsInterface -DA:40,21 -DA:41,20 -DA:42,19 -DA:43,13 -DA:44,12 -DA:45,11 -DA:52,1924 -FN:52,RuleConditionalTransferLight._authorizeTransferApproval -FNDA:1924,RuleConditionalTransferLight._authorizeTransferApproval -DA:54,25 -FN:54,RuleConditionalTransferLight._onlyComplianceManager -FNDA:25,RuleConditionalTransferLight._onlyComplianceManager -FNF:3 -FNH:3 +DA:41,48 +FN:41,RuleConditionalTransferLight.supportsInterface +FNDA:48,RuleConditionalTransferLight.supportsInterface +DA:48,48 +DA:49,47 +DA:50,46 +DA:51,30 +DA:52,29 +DA:62,58 +FN:62,RuleConditionalTransferLight._onlyComplianceManager +FNDA:58,RuleConditionalTransferLight._onlyComplianceManager +DA:67,7607 +FN:67,RuleConditionalTransferLight._authorizeTransferApproval +FNDA:7607,RuleConditionalTransferLight._authorizeTransferApproval +DA:72,3 +FN:72,RuleConditionalTransferLight._authorizeComplianceBindingChange +FNDA:3,RuleConditionalTransferLight._authorizeComplianceBindingChange +FNF:4 +FNH:4 LF:9 LH:9 BRF:0 BRH:0 end_of_record TN: +SF:src/rules/operation/RuleConditionalTransferLightMultiToken.sol +DA:33,6 +FN:33,RuleConditionalTransferLightMultiToken.supportsInterface +FNDA:6,RuleConditionalTransferLightMultiToken.supportsInterface +DA:40,6 +DA:41,6 +DA:42,6 +DA:43,4 +DA:44,4 +DA:50,38 +FN:50,RuleConditionalTransferLightMultiToken._onlyComplianceManager +FNDA:38,RuleConditionalTransferLightMultiToken._onlyComplianceManager +DA:55,31 +FN:55,RuleConditionalTransferLightMultiToken._authorizeTransferApproval +FNDA:31,RuleConditionalTransferLightMultiToken._authorizeTransferApproval +FNF:3 +FNH:3 +LF:8 +LH:8 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol +DA:33,5 +FN:33,RuleConditionalTransferLightMultiTokenOwnable2Step.supportsInterface +FNDA:5,RuleConditionalTransferLightMultiTokenOwnable2Step.supportsInterface +DA:39,5 +DA:40,2 +DA:41,2 +DA:42,2 +DA:43,2 +DA:49,0 +FN:49,RuleConditionalTransferLightMultiTokenOwnable2Step._onlyComplianceManager +FNDA:0,RuleConditionalTransferLightMultiTokenOwnable2Step._onlyComplianceManager +DA:54,0 +FN:54,RuleConditionalTransferLightMultiTokenOwnable2Step._authorizeTransferApproval +FNDA:0,RuleConditionalTransferLightMultiTokenOwnable2Step._authorizeTransferApproval +FNF:3 +FNH:1 +LF:8 +LH:6 +BRF:0 +BRH:0 +end_of_record +TN: SF:src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol -DA:29,7 -FN:29,RuleConditionalTransferLightOwnable2Step.supportsInterface -FNDA:7,RuleConditionalTransferLightOwnable2Step.supportsInterface -DA:30,7 -DA:31,6 -DA:32,5 -DA:33,4 -DA:34,3 -DA:35,2 -DA:42,4 -FN:42,RuleConditionalTransferLightOwnable2Step._authorizeTransferApproval -FNDA:4,RuleConditionalTransferLightOwnable2Step._authorizeTransferApproval -DA:44,3 -FN:44,RuleConditionalTransferLightOwnable2Step._onlyComplianceManager +DA:40,12 +FN:40,RuleConditionalTransferLightOwnable2Step.supportsInterface +FNDA:12,RuleConditionalTransferLightOwnable2Step.supportsInterface +DA:46,12 +DA:47,8 +DA:48,7 +DA:49,6 +DA:50,4 +DA:60,3 +FN:60,RuleConditionalTransferLightOwnable2Step._onlyComplianceManager FNDA:3,RuleConditionalTransferLightOwnable2Step._onlyComplianceManager -FNF:3 +DA:65,4 +FN:65,RuleConditionalTransferLightOwnable2Step._authorizeTransferApproval +FNDA:4,RuleConditionalTransferLightOwnable2Step._authorizeTransferApproval +DA:70,0 +FN:70,RuleConditionalTransferLightOwnable2Step._authorizeComplianceBindingChange +FNDA:0,RuleConditionalTransferLightOwnable2Step._authorizeComplianceBindingChange +FNF:4 FNH:3 LF:9 -LH:9 +LH:8 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/rules/operation/RuleMintAllowance.sol +DA:38,34 +FN:38,RuleMintAllowance.supportsInterface +FNDA:34,RuleMintAllowance.supportsInterface +DA:47,34 +DA:48,33 +DA:49,32 +DA:50,21 +DA:60,312 +FN:60,RuleMintAllowance._onlyComplianceManager +FNDA:312,RuleMintAllowance._onlyComplianceManager +DA:65,10070 +FN:65,RuleMintAllowance._authorizeSetMintAllowance +FNDA:10070,RuleMintAllowance._authorizeSetMintAllowance +DA:70,4 +FN:70,RuleMintAllowance._authorizeComplianceBindingChange +FNDA:4,RuleMintAllowance._authorizeComplianceBindingChange +FNF:4 +FNH:4 +LF:8 +LH:8 +BRF:0 +BRH:0 +end_of_record +TN: +SF:src/rules/operation/RuleMintAllowanceOwnable2Step.sol +DA:36,8 +FN:36,RuleMintAllowanceOwnable2Step.supportsInterface +FNDA:8,RuleMintAllowanceOwnable2Step.supportsInterface +DA:44,8 +DA:45,6 +DA:46,5 +DA:47,4 +DA:57,11 +FN:57,RuleMintAllowanceOwnable2Step._onlyComplianceManager +FNDA:11,RuleMintAllowanceOwnable2Step._onlyComplianceManager +DA:62,6 +FN:62,RuleMintAllowanceOwnable2Step._authorizeSetMintAllowance +FNDA:6,RuleMintAllowanceOwnable2Step._authorizeSetMintAllowance +DA:67,2 +FN:67,RuleMintAllowanceOwnable2Step._authorizeComplianceBindingChange +FNDA:2,RuleMintAllowanceOwnable2Step._authorizeComplianceBindingChange +FNF:4 +FNH:4 +LF:8 +LH:8 BRF:0 BRH:0 end_of_record TN: SF:src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol -DA:20,4 -FN:20,RuleConditionalTransferLightApprovalBase.onlyTransferApprover +DA:22,4 +FN:22,RuleConditionalTransferLightApprovalBase.onlyTransferApprover FNDA:4,RuleConditionalTransferLightApprovalBase.onlyTransferApprover -DA:21,4 -DA:25,3 -FN:25,RuleConditionalTransferLightApprovalBase.onlyTransferExecutor +DA:23,4 +DA:27,3 +FN:27,RuleConditionalTransferLightApprovalBase.onlyTransferExecutor FNDA:3,RuleConditionalTransferLightApprovalBase.onlyTransferExecutor -DA:26,3 -DA:30,0 -FN:30,RuleConditionalTransferLightApprovalBase._authorizeTransferApproval -FNDA:0,RuleConditionalTransferLightApprovalBase._authorizeTransferApproval -DA:32,0 -FN:32,RuleConditionalTransferLightApprovalBase._authorizeTransferExecution -FNDA:0,RuleConditionalTransferLightApprovalBase._authorizeTransferExecution -DA:38,3 -FN:38,RuleConditionalTransferLightApprovalBase.transferred +DA:28,3 +DA:40,3 +FN:40,RuleConditionalTransferLightApprovalBase.transferred FNDA:3,RuleConditionalTransferLightApprovalBase.transferred -DA:39,3 -DA:46,1917 -FN:46,RuleConditionalTransferLightApprovalBase.approveTransfer -FNDA:1917,RuleConditionalTransferLightApprovalBase.approveTransfer -DA:47,1918 -DA:48,1918 -DA:49,1918 -DA:52,4 -FN:52,RuleConditionalTransferLightApprovalBase.cancelTransferApproval -FNDA:4,RuleConditionalTransferLightApprovalBase.cancelTransferApproval -DA:53,3 -DA:54,3 -DA:55,3 -BRDA:55,0,0,1 -BRDA:55,0,1,2 -DA:56,2 -DA:57,2 -DA:60,263 -FN:60,RuleConditionalTransferLightApprovalBase.approvedCount -FNDA:263,RuleConditionalTransferLightApprovalBase.approvedCount -DA:61,263 -DA:62,263 -DA:69,3 -FN:69,RuleConditionalTransferLightApprovalBase._transferredFromContext +DA:41,3 +DA:54,6210 +FN:54,RuleConditionalTransferLightApprovalBase.approveTransfer +FNDA:6210,RuleConditionalTransferLightApprovalBase.approveTransfer +DA:55,6213 +DA:56,6213 +DA:57,6213 +DA:66,1386 +FN:66,RuleConditionalTransferLightApprovalBase.cancelTransferApproval +FNDA:1386,RuleConditionalTransferLightApprovalBase.cancelTransferApproval +DA:67,1385 +DA:68,1385 +DA:69,1385 +BRDA:69,0,0,1 +BRDA:69,0,1,1384 +DA:70,1384 +DA:71,1384 +DA:86,4 +FN:86,RuleConditionalTransferLightApprovalBase.resetApproval +FNDA:4,RuleConditionalTransferLightApprovalBase.resetApproval +DA:92,3 +DA:93,3 +DA:94,3 +BRDA:94,1,0,1 +BRDA:94,1,1,2 +DA:95,2 +DA:96,2 +DA:106,9207 +FN:106,RuleConditionalTransferLightApprovalBase.approvedCount +FNDA:9207,RuleConditionalTransferLightApprovalBase.approvedCount +DA:107,9207 +DA:108,9207 +DA:119,3 +FN:119,RuleConditionalTransferLightApprovalBase._transferredFromContext FNDA:3,RuleConditionalTransferLightApprovalBase._transferredFromContext -DA:70,3 -DA:73,850 -FN:73,RuleConditionalTransferLightApprovalBase._transferred -FNDA:850,RuleConditionalTransferLightApprovalBase._transferred -DA:74,850 -BRDA:74,1,0,850 -DA:75,850 -DA:77,846 -DA:78,846 -DA:80,846 -BRDA:80,2,0,3 -BRDA:80,2,1,843 -DA:82,843 -DA:83,843 -DA:86,3039 -FN:86,RuleConditionalTransferLightApprovalBase._transferHash -FNDA:3039,RuleConditionalTransferLightApprovalBase._transferHash -DA:89,3039 -DA:90,3039 -DA:91,3039 -DA:92,3039 -DA:93,3039 -FNF:11 -FNH:9 -LF:37 -LH:35 -BRF:5 -BRH:5 +DA:120,3 +DA:130,6353 +FN:130,RuleConditionalTransferLightApprovalBase._transferred +FNDA:6353,RuleConditionalTransferLightApprovalBase._transferred +DA:131,6353 +BRDA:131,2,0,6353 +DA:132,6353 +DA:134,2307 +DA:135,2307 +DA:137,2307 +BRDA:137,3,0,5 +BRDA:137,3,1,2302 +DA:139,2302 +DA:140,2302 +DA:150,19124 +FN:150,RuleConditionalTransferLightApprovalBase._transferHash +FNDA:19124,RuleConditionalTransferLightApprovalBase._transferHash +DA:153,19124 +DA:154,19124 +DA:155,19124 +DA:156,19124 +DA:157,19124 +DA:164,0 +FN:164,RuleConditionalTransferLightApprovalBase._authorizeTransferApproval +FNDA:0,RuleConditionalTransferLightApprovalBase._authorizeTransferApproval +DA:169,0 +FN:169,RuleConditionalTransferLightApprovalBase._authorizeTransferExecution +FNDA:0,RuleConditionalTransferLightApprovalBase._authorizeTransferExecution +FNF:12 +FNH:10 +LF:43 +LH:41 +BRF:7 +BRH:7 end_of_record TN: SF:src/rules/operation/abstract/RuleConditionalTransferLightBase.sol -DA:32,1 -FN:32,RuleConditionalTransferLightBase.canReturnTransferRestrictionCode -FNDA:1,RuleConditionalTransferLightBase.canReturnTransferRestrictionCode -DA:33,1 -DA:36,2 -FN:36,RuleConditionalTransferLightBase.messageForTransferRestriction -FNDA:2,RuleConditionalTransferLightBase.messageForTransferRestriction -DA:42,2 -BRDA:42,0,0,1 -DA:43,1 -DA:45,1 -DA:48,1 -FN:48,RuleConditionalTransferLightBase.created +DA:59,1 +FN:59,RuleConditionalTransferLightBase.created FNDA:1,RuleConditionalTransferLightBase.created -DA:49,1 -DA:52,1 -FN:52,RuleConditionalTransferLightBase.destroyed +DA:60,1 +DA:68,1 +FN:68,RuleConditionalTransferLightBase.destroyed FNDA:1,RuleConditionalTransferLightBase.destroyed -DA:53,1 -DA:66,4 -FN:66,RuleConditionalTransferLightBase.approveAndTransferIfAllowed -FNDA:4,RuleConditionalTransferLightBase.approveAndTransferIfAllowed -DA:71,4 -DA:72,4 -BRDA:72,1,0,1 -BRDA:72,1,1,3 -DA:74,3 -DA:76,3 -DA:77,3 -BRDA:77,2,0,1 -BRDA:77,2,1,2 -DA:79,2 -DA:80,1 -DA:83,846 -FN:83,RuleConditionalTransferLightBase.transferred.0 -FNDA:846,RuleConditionalTransferLightBase.transferred.0 -DA:88,844 +DA:69,1 +DA:75,1 +FN:75,RuleConditionalTransferLightBase.canReturnTransferRestrictionCode +FNDA:1,RuleConditionalTransferLightBase.canReturnTransferRestrictionCode +DA:76,1 +DA:82,2 +FN:82,RuleConditionalTransferLightBase.messageForTransferRestriction +FNDA:2,RuleConditionalTransferLightBase.messageForTransferRestriction +DA:88,2 +BRDA:88,0,0,1 +DA:89,1 DA:91,1 -FN:91,RuleConditionalTransferLightBase.transferred.1 -FNDA:1,RuleConditionalTransferLightBase.transferred.1 -DA:102,1 -DA:114,27 -FN:114,RuleConditionalTransferLightBase.bindToken -FNDA:27,RuleConditionalTransferLightBase.bindToken -DA:115,26 -BRDA:115,3,0,1 -BRDA:115,3,1,25 -DA:116,25 -DA:119,7 -FN:119,RuleConditionalTransferLightBase.detectTransferRestriction +DA:113,6 +FN:113,RuleConditionalTransferLightBase.approveAndTransferIfAllowed +FNDA:6,RuleConditionalTransferLightBase.approveAndTransferIfAllowed +DA:118,6 +DA:119,6 +BRDA:119,1,0,1 +BRDA:119,1,1,5 +DA:121,5 +DA:123,5 +DA:124,4 +BRDA:124,2,0,1 +BRDA:124,2,1,3 +DA:126,3 +DA:127,2 +DA:133,6347 +FN:133,RuleConditionalTransferLightBase.transferred.0 +FNDA:6347,RuleConditionalTransferLightBase.transferred.0 +DA:138,6342 +DA:144,7 +FN:144,RuleConditionalTransferLightBase.transferred.1 +FNDA:7,RuleConditionalTransferLightBase.transferred.1 +DA:155,6 +DA:177,45 +FN:177,RuleConditionalTransferLightBase.bindToken +FNDA:45,RuleConditionalTransferLightBase.bindToken +DA:178,44 +BRDA:178,3,0,1 +BRDA:178,3,1,43 +DA:179,43 +DA:208,13 +FN:208,RuleConditionalTransferLightBase.bindRuleEngine +FNDA:13,RuleConditionalTransferLightBase.bindRuleEngine +DA:209,12 +BRDA:209,4,0,1 +BRDA:209,4,1,11 +DA:210,11 +BRDA:210,5,0,1 +BRDA:210,5,1,10 +DA:211,10 +DA:212,10 +DA:219,3 +FN:219,RuleConditionalTransferLightBase.unbindRuleEngine +FNDA:3,RuleConditionalTransferLightBase.unbindRuleEngine +DA:220,2 +DA:221,2 +BRDA:221,6,0,1 +BRDA:221,6,1,1 +DA:222,1 +DA:223,1 +DA:231,8 +FN:231,RuleConditionalTransferLightBase.isTransferExecutor +FNDA:8,RuleConditionalTransferLightBase.isTransferExecutor +DA:232,6365 +DA:238,7 +FN:238,RuleConditionalTransferLightBase.detectTransferRestriction FNDA:7,RuleConditionalTransferLightBase.detectTransferRestriction -DA:125,13 -BRDA:125,4,0,4 -DA:126,4 -DA:128,9 -DA:129,9 -BRDA:129,5,0,6 -DA:130,6 -DA:132,3 -DA:135,1 -FN:135,RuleConditionalTransferLightBase.detectTransferRestrictionFrom +DA:244,13 +BRDA:244,7,0,4 +DA:245,4 +DA:247,9 +DA:248,9 +BRDA:248,8,0,6 +DA:249,6 +DA:251,3 +DA:257,1 +FN:257,RuleConditionalTransferLightBase.detectTransferRestrictionFrom FNDA:1,RuleConditionalTransferLightBase.detectTransferRestrictionFrom -DA:147,2 -DA:150,4 -FN:150,RuleConditionalTransferLightBase.canTransfer +DA:269,2 +DA:275,4 +FN:275,RuleConditionalTransferLightBase.canTransfer FNDA:4,RuleConditionalTransferLightBase.canTransfer -DA:156,4 -DA:159,1 -FN:159,RuleConditionalTransferLightBase.canTransferFrom +DA:281,4 +DA:287,1 +FN:287,RuleConditionalTransferLightBase.canTransferFrom FNDA:1,RuleConditionalTransferLightBase.canTransferFrom -DA:165,1 -DA:173,850 -FN:173,RuleConditionalTransferLightBase._authorizeTransferExecution -FNDA:850,RuleConditionalTransferLightBase._authorizeTransferExecution -DA:174,850 -BRDA:174,6,0,2 -BRDA:174,6,1,848 -FNF:13 -FNH:13 -LF:40 -LH:40 -BRF:11 -BRH:11 +DA:293,1 +DA:306,6357 +FN:306,RuleConditionalTransferLightBase._authorizeTransferExecution +FNDA:6357,RuleConditionalTransferLightBase._authorizeTransferExecution +DA:307,6357 +BRDA:307,9,0,6 +BRDA:307,9,1,6351 +FNF:16 +FNH:16 +LF:52 +LH:52 +BRF:17 +BRH:17 +end_of_record +TN: +SF:src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol +DA:34,24 +FN:34,RuleConditionalTransferLightMultiTokenBase.onlyTransferApprover +FNDA:24,RuleConditionalTransferLightMultiTokenBase.onlyTransferApprover +DA:35,24 +DA:39,3 +FN:39,RuleConditionalTransferLightMultiTokenBase.onlyTransferExecutor +FNDA:3,RuleConditionalTransferLightMultiTokenBase.onlyTransferExecutor +DA:40,3 +DA:49,2 +FN:49,RuleConditionalTransferLightMultiTokenBase.created +FNDA:2,RuleConditionalTransferLightMultiTokenBase.created +DA:50,1 +DA:58,2 +FN:58,RuleConditionalTransferLightMultiTokenBase.destroyed +FNDA:2,RuleConditionalTransferLightMultiTokenBase.destroyed +DA:59,1 +DA:66,3 +FN:66,RuleConditionalTransferLightMultiTokenBase.transferred.0 +FNDA:3,RuleConditionalTransferLightMultiTokenBase.transferred.0 +DA:67,3 +DA:73,2 +FN:73,RuleConditionalTransferLightMultiTokenBase.canReturnTransferRestrictionCode +FNDA:2,RuleConditionalTransferLightMultiTokenBase.canReturnTransferRestrictionCode +DA:74,2 +DA:80,2 +FN:80,RuleConditionalTransferLightMultiTokenBase.messageForTransferRestriction +FNDA:2,RuleConditionalTransferLightMultiTokenBase.messageForTransferRestriction +DA:86,2 +BRDA:86,0,0,1 +DA:87,1 +DA:89,1 +DA:99,24 +FN:99,RuleConditionalTransferLightMultiTokenBase.approveTransfer +FNDA:24,RuleConditionalTransferLightMultiTokenBase.approveTransfer +DA:100,24 +DA:110,3 +FN:110,RuleConditionalTransferLightMultiTokenBase.cancelTransferApproval +FNDA:3,RuleConditionalTransferLightMultiTokenBase.cancelTransferApproval +DA:114,2 +DA:126,1 +FN:126,RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed +FNDA:1,RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed +DA:131,1 +BRDA:131,1,0,- +BRDA:131,1,1,1 +DA:133,1 +DA:135,1 +DA:136,1 +BRDA:136,2,0,- +BRDA:136,2,1,1 +DA:140,1 +DA:141,1 +DA:147,4 +FN:147,RuleConditionalTransferLightMultiTokenBase.transferred.1 +FNDA:4,RuleConditionalTransferLightMultiTokenBase.transferred.1 +DA:152,4 +DA:158,6 +FN:158,RuleConditionalTransferLightMultiTokenBase.transferred.2 +FNDA:6,RuleConditionalTransferLightMultiTokenBase.transferred.2 +DA:169,6 +DA:187,3 +FN:187,RuleConditionalTransferLightMultiTokenBase.resetApproval +FNDA:3,RuleConditionalTransferLightMultiTokenBase.resetApproval +DA:193,2 +DA:194,2 +DA:195,2 +BRDA:195,3,0,1 +BRDA:195,3,1,1 +DA:196,1 +DA:197,1 +DA:208,16 +FN:208,RuleConditionalTransferLightMultiTokenBase.approvedCount +FNDA:16,RuleConditionalTransferLightMultiTokenBase.approvedCount +DA:209,16 +DA:210,16 +DA:222,265 +FN:222,RuleConditionalTransferLightMultiTokenBase.detectTransferRestriction +FNDA:265,RuleConditionalTransferLightMultiTokenBase.detectTransferRestriction +DA:228,270 +DA:242,263 +FN:242,RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionForToken +FNDA:263,RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionForToken +DA:248,263 +DA:261,5 +FN:261,RuleConditionalTransferLightMultiTokenBase.canTransferForToken +FNDA:5,RuleConditionalTransferLightMultiTokenBase.canTransferForToken +DA:267,5 +DA:274,2 +FN:274,RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionFrom +FNDA:2,RuleConditionalTransferLightMultiTokenBase.detectTransferRestrictionFrom +DA:286,4 +DA:294,1 +FN:294,RuleConditionalTransferLightMultiTokenBase.canTransfer +FNDA:1,RuleConditionalTransferLightMultiTokenBase.canTransfer +DA:300,1 +DA:306,2 +FN:306,RuleConditionalTransferLightMultiTokenBase.canTransferFrom +FNDA:2,RuleConditionalTransferLightMultiTokenBase.canTransferFrom +DA:312,2 +DA:328,538 +FN:328,RuleConditionalTransferLightMultiTokenBase._detectTransferRestrictionForToken +FNDA:538,RuleConditionalTransferLightMultiTokenBase._detectTransferRestrictionForToken +DA:334,538 +BRDA:334,4,0,2 +DA:335,2 +DA:338,536 +BRDA:338,5,0,7 +DA:339,7 +DA:342,529 +BRDA:342,6,0,519 +DA:343,519 +DA:346,10 +DA:360,38 +FN:360,RuleConditionalTransferLightMultiTokenBase._authorizeComplianceBindingChange +FNDA:38,RuleConditionalTransferLightMultiTokenBase._authorizeComplianceBindingChange +DA:361,38 +DA:371,25 +FN:371,RuleConditionalTransferLightMultiTokenBase._approveTransfer +FNDA:25,RuleConditionalTransferLightMultiTokenBase._approveTransfer +DA:372,25 +BRDA:372,7,0,2 +BRDA:372,7,1,23 +DA:373,23 +DA:374,23 +DA:375,23 +DA:385,2 +FN:385,RuleConditionalTransferLightMultiTokenBase._cancelTransferApproval +FNDA:2,RuleConditionalTransferLightMultiTokenBase._cancelTransferApproval +DA:386,2 +BRDA:386,8,0,- +BRDA:386,8,1,2 +DA:387,2 +DA:388,2 +DA:390,2 +BRDA:390,9,0,1 +BRDA:390,9,1,1 +DA:392,1 +DA:393,1 +DA:404,15 +FN:404,RuleConditionalTransferLightMultiTokenBase._transferred +FNDA:15,RuleConditionalTransferLightMultiTokenBase._transferred +DA:405,15 +BRDA:405,10,0,15 +DA:406,15 +DA:409,9 +DA:410,9 +DA:412,9 +BRDA:412,11,0,3 +BRDA:412,11,1,6 +DA:414,6 +DA:415,6 +DA:421,13 +FN:421,RuleConditionalTransferLightMultiTokenBase._authorizeTransferExecution +FNDA:13,RuleConditionalTransferLightMultiTokenBase._authorizeTransferExecution +DA:422,13 +BRDA:422,12,0,- +BRDA:422,12,1,13 +DA:431,0 +FN:431,RuleConditionalTransferLightMultiTokenBase._authorizeTransferApproval +FNDA:0,RuleConditionalTransferLightMultiTokenBase._authorizeTransferApproval +DA:441,581 +FN:441,RuleConditionalTransferLightMultiTokenBase._transferHash +FNDA:581,RuleConditionalTransferLightMultiTokenBase._transferHash +DA:448,581 +DA:449,581 +DA:450,581 +DA:451,581 +DA:452,581 +DA:453,581 +FNF:28 +FNH:27 +LF:92 +LH:91 +BRF:21 +BRH:17 +end_of_record +TN: +SF:src/rules/operation/abstract/RuleMintAllowanceBase.sol +DA:51,4 +FN:51,RuleMintAllowanceBase.onlyAllowanceOperator +FNDA:4,RuleMintAllowanceBase.onlyAllowanceOperator +DA:52,4 +DA:63,1 +FN:63,RuleMintAllowanceBase.created +FNDA:1,RuleMintAllowanceBase.created +DA:68,1 +FN:68,RuleMintAllowanceBase.destroyed +FNDA:1,RuleMintAllowanceBase.destroyed +DA:73,2 +FN:73,RuleMintAllowanceBase.canReturnTransferRestrictionCode +FNDA:2,RuleMintAllowanceBase.canReturnTransferRestrictionCode +DA:74,2 +DA:86,3753 +FN:86,RuleMintAllowanceBase.setMintAllowance +FNDA:3753,RuleMintAllowanceBase.setMintAllowance +DA:87,3751 +DA:95,3241 +FN:95,RuleMintAllowanceBase.increaseMintAllowance +FNDA:3241,RuleMintAllowanceBase.increaseMintAllowance +DA:96,3240 +DA:97,3240 +DA:98,3240 +DA:107,3078 +FN:107,RuleMintAllowanceBase.decreaseMintAllowance +FNDA:3078,RuleMintAllowanceBase.decreaseMintAllowance +DA:108,3077 +DA:109,3077 +BRDA:109,0,0,1 +BRDA:109,0,1,3076 +DA:110,3076 +DA:111,3076 +DA:112,3076 +DA:124,4 +FN:124,RuleMintAllowanceBase.clearMintAllowances +FNDA:4,RuleMintAllowanceBase.clearMintAllowances +DA:125,3 +DA:126,7 +DA:142,323 +FN:142,RuleMintAllowanceBase.bindToken +FNDA:323,RuleMintAllowanceBase.bindToken +DA:143,321 +BRDA:143,1,0,2 +BRDA:143,1,1,319 +DA:144,319 +DA:154,3 +FN:154,RuleMintAllowanceBase.transferred.0 +FNDA:3,RuleMintAllowanceBase.transferred.0 +DA:160,2 +DA:171,6849 +FN:171,RuleMintAllowanceBase.transferred.1 +FNDA:6849,RuleMintAllowanceBase.transferred.1 +DA:177,6848 +DA:183,2 +FN:183,RuleMintAllowanceBase.messageForTransferRestriction +FNDA:2,RuleMintAllowanceBase.messageForTransferRestriction +DA:189,2 +BRDA:189,2,0,1 +DA:190,1 +DA:192,1 +DA:200,5 +FN:200,RuleMintAllowanceBase.detectTransferRestriction +FNDA:5,RuleMintAllowanceBase.detectTransferRestriction +DA:207,5 +DA:213,9 +FN:213,RuleMintAllowanceBase.detectTransferRestrictionFrom +FNDA:9,RuleMintAllowanceBase.detectTransferRestrictionFrom +DA:220,15 +DA:227,3 +FN:227,RuleMintAllowanceBase.canTransfer +FNDA:3,RuleMintAllowanceBase.canTransfer +DA:234,3 +DA:240,6 +FN:240,RuleMintAllowanceBase.canTransferFrom +FNDA:6,RuleMintAllowanceBase.canTransferFrom +DA:247,6 +DA:258,2 +FN:258,RuleMintAllowanceBase._transferred +FNDA:2,RuleMintAllowanceBase._transferred +DA:270,6848 +FN:270,RuleMintAllowanceBase._transferredFrom +FNDA:6848,RuleMintAllowanceBase._transferredFrom +DA:271,6848 +BRDA:271,3,0,6848 +DA:272,6848 +DA:274,3577 +DA:275,3577 +BRDA:275,4,0,252 +BRDA:275,4,1,3325 +DA:276,3325 +DA:277,3325 +DA:278,3325 +DA:286,3758 +FN:286,RuleMintAllowanceBase._setMintAllowance +FNDA:3758,RuleMintAllowanceBase._setMintAllowance +DA:287,3758 +DA:288,3758 +DA:299,15 +FN:299,RuleMintAllowanceBase._detectTransferRestrictionFrom +FNDA:15,RuleMintAllowanceBase._detectTransferRestrictionFrom +DA:305,15 +BRDA:305,5,0,7 +DA:306,7 +DA:308,8 +DA:314,0 +FN:314,RuleMintAllowanceBase._authorizeSetMintAllowance +FNDA:0,RuleMintAllowanceBase._authorizeSetMintAllowance +FNF:21 +FNH:20 +LF:57 +LH:56 +BRF:9 +BRH:9 end_of_record TN: SF:src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol -DA:40,275 +DA:40,278 FN:40,RuleAddressSet.onlyAddressListAdd -FNDA:275,RuleAddressSet.onlyAddressListAdd -DA:41,275 +FNDA:278,RuleAddressSet.onlyAddressListAdd +DA:41,278 DA:45,12 FN:45,RuleAddressSet.onlyAddressListRemove FNDA:12,RuleAddressSet.onlyAddressListRemove DA:46,12 -DA:50,0 -FN:50,RuleAddressSet._authorizeAddressListAdd -FNDA:0,RuleAddressSet._authorizeAddressListAdd -DA:52,0 -FN:52,RuleAddressSet._authorizeAddressListRemove -FNDA:0,RuleAddressSet._authorizeAddressListRemove -DA:65,275 -FN:65,RuleAddressSet.addAddresses -FNDA:275,RuleAddressSet.addAddresses -DA:66,274 -DA:67,274 -DA:77,260 -FN:77,RuleAddressSet.removeAddresses +DA:61,278 +FN:61,RuleAddressSet.addAddresses +FNDA:278,RuleAddressSet.addAddresses +DA:62,277 +DA:63,275 +DA:73,260 +FN:73,RuleAddressSet.removeAddresses FNDA:260,RuleAddressSet.removeAddresses -DA:78,259 -DA:79,259 -DA:89,117 -FN:89,RuleAddressSet.addAddress -FNDA:117,RuleAddressSet.addAddress -DA:90,112 -BRDA:90,0,0,1 -BRDA:90,0,1,111 -DA:91,111 -DA:92,111 -DA:102,12 -FN:102,RuleAddressSet.removeAddress +DA:74,259 +DA:75,259 +DA:85,132 +FN:85,RuleAddressSet.addAddress +FNDA:132,RuleAddressSet.addAddress +DA:86,127 +BRDA:86,0,0,2 +BRDA:86,0,1,125 +DA:87,125 +BRDA:87,1,0,1 +BRDA:87,1,1,124 +DA:88,124 +DA:89,124 +DA:99,12 +FN:99,RuleAddressSet.removeAddress FNDA:12,RuleAddressSet.removeAddress -DA:103,7 -BRDA:103,1,0,1 -BRDA:103,1,1,6 -DA:104,6 -DA:105,6 -DA:112,536 -FN:112,RuleAddressSet.listedAddressCount -FNDA:536,RuleAddressSet.listedAddressCount -DA:113,536 -DA:121,2 -FN:121,RuleAddressSet.contains -FNDA:2,RuleAddressSet.contains -DA:122,2 -DA:130,73 -FN:130,RuleAddressSet.isAddressListed -FNDA:73,RuleAddressSet.isAddressListed -DA:131,393 -DA:139,108 -FN:139,RuleAddressSet.areAddressesListed -FNDA:108,RuleAddressSet.areAddressesListed -DA:140,108 -DA:141,108 -DA:142,244 -DA:151,931 -FN:151,RuleAddressSet._msgSender -FNDA:931,RuleAddressSet._msgSender -DA:152,931 -DA:156,6 -FN:156,RuleAddressSet._msgData +DA:100,7 +BRDA:100,2,0,1 +BRDA:100,2,1,6 +DA:101,6 +DA:102,6 +DA:109,543 +FN:109,RuleAddressSet.listedAddressCount +FNDA:543,RuleAddressSet.listedAddressCount +DA:110,543 +DA:118,4 +FN:118,RuleAddressSet.contains +FNDA:4,RuleAddressSet.contains +DA:119,4 +DA:127,79 +FN:127,RuleAddressSet.isAddressListed +FNDA:79,RuleAddressSet.isAddressListed +DA:128,577 +DA:136,155 +FN:136,RuleAddressSet.areAddressesListed +FNDA:155,RuleAddressSet.areAddressesListed +DA:137,155 +DA:138,155 +DA:139,345 +DA:150,0 +FN:150,RuleAddressSet._authorizeAddressListAdd +FNDA:0,RuleAddressSet._authorizeAddressListAdd +DA:155,0 +FN:155,RuleAddressSet._authorizeAddressListRemove +FNDA:0,RuleAddressSet._authorizeAddressListRemove +DA:160,994 +FN:160,RuleAddressSet._msgSender +FNDA:994,RuleAddressSet._msgSender +DA:161,994 +DA:167,6 +FN:167,RuleAddressSet._msgData FNDA:6,RuleAddressSet._msgData -DA:157,6 -DA:161,939 -FN:161,RuleAddressSet._contextSuffixLength -FNDA:939,RuleAddressSet._contextSuffixLength -DA:162,939 +DA:168,6 +DA:174,1002 +FN:174,RuleAddressSet._contextSuffixLength +FNDA:1002,RuleAddressSet._contextSuffixLength +DA:175,1002 FNF:15 FNH:13 -LF:36 -LH:34 -BRF:4 -BRH:4 +LF:37 +LH:35 +BRF:6 +BRH:6 end_of_record TN: SF:src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol -DA:38,274 -FN:38,RuleAddressSetInternal._addAddresses -FNDA:274,RuleAddressSetInternal._addAddresses -DA:39,274 -DA:40,806 -BRDA:40,0,0,548 -BRDA:40,0,1,258 -DA:41,548 -DA:43,258 -DA:57,259 -FN:57,RuleAddressSetInternal._removeAddresses +DA:41,277 +FN:41,RuleAddressSetInternal._addAddresses +FNDA:277,RuleAddressSetInternal._addAddresses +DA:42,277 +DA:49,813 +BRDA:49,0,0,2 +BRDA:49,0,1,811 +DA:50,811 +BRDA:50,1,0,551 +BRDA:50,1,1,260 +DA:51,551 +DA:53,260 +DA:67,259 +FN:67,RuleAddressSetInternal._removeAddresses FNDA:259,RuleAddressSetInternal._removeAddresses -DA:61,259 -DA:62,775 -BRDA:62,1,0,518 -BRDA:62,1,1,257 -DA:63,518 -DA:65,257 -DA:74,113 -FN:74,RuleAddressSetInternal._addAddress -FNDA:113,RuleAddressSetInternal._addAddress -DA:75,113 -DA:82,6 -FN:82,RuleAddressSetInternal._removeAddress +DA:71,259 +DA:72,775 +BRDA:72,2,0,518 +BRDA:72,2,1,257 +DA:73,518 +DA:75,257 +DA:84,124 +FN:84,RuleAddressSetInternal._addAddress +FNDA:124,RuleAddressSetInternal._addAddress +DA:85,124 +DA:92,6 +FN:92,RuleAddressSetInternal._removeAddress FNDA:6,RuleAddressSetInternal._removeAddress -DA:83,6 -DA:90,536 -FN:90,RuleAddressSetInternal._listedAddressCount -FNDA:536,RuleAddressSetInternal._listedAddressCount -DA:91,536 -DA:99,773 -FN:99,RuleAddressSetInternal._isAddressListed -FNDA:773,RuleAddressSetInternal._isAddressListed -DA:100,773 +DA:93,6 +DA:100,543 +FN:100,RuleAddressSetInternal._listedAddressCount +FNDA:543,RuleAddressSetInternal._listedAddressCount +DA:101,543 +DA:109,1093 +FN:109,RuleAddressSetInternal._isAddressListed +FNDA:1093,RuleAddressSetInternal._isAddressListed +DA:110,1093 FNF:6 FNH:6 -LF:18 -LH:18 -BRF:4 -BRH:4 +LF:19 +LH:19 +BRF:6 +BRH:6 end_of_record TN: SF:src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol -DA:33,4 -FN:33,RuleERC2980Internal._addWhitelistAddresses +DA:44,4 +FN:44,RuleERC2980Internal._addWhitelistAddresses FNDA:4,RuleERC2980Internal._addWhitelistAddresses -DA:37,4 -DA:38,6 -BRDA:38,0,0,5 -BRDA:38,0,1,1 -DA:39,5 -DA:41,1 -DA:46,3 -FN:46,RuleERC2980Internal._removeWhitelistAddresses +DA:48,4 +DA:51,6 +BRDA:51,0,0,- +BRDA:51,0,1,6 +DA:52,6 +BRDA:52,1,0,5 +BRDA:52,1,1,1 +DA:53,5 +DA:55,1 +DA:66,3 +FN:66,RuleERC2980Internal._removeWhitelistAddresses FNDA:3,RuleERC2980Internal._removeWhitelistAddresses -DA:50,3 -DA:51,3 -BRDA:51,1,0,2 -BRDA:51,1,1,1 -DA:52,2 -DA:54,1 -DA:59,43 -FN:59,RuleERC2980Internal._addWhitelistAddress -FNDA:43,RuleERC2980Internal._addWhitelistAddress -DA:60,43 -DA:63,4 -FN:63,RuleERC2980Internal._removeWhitelistAddress +DA:70,3 +DA:71,3 +BRDA:71,2,0,2 +BRDA:71,2,1,1 +DA:72,2 +DA:74,1 +DA:83,44 +FN:83,RuleERC2980Internal._addWhitelistAddress +FNDA:44,RuleERC2980Internal._addWhitelistAddress +DA:84,44 +DA:91,4 +FN:91,RuleERC2980Internal._removeWhitelistAddress FNDA:4,RuleERC2980Internal._removeWhitelistAddress -DA:64,4 -DA:67,89 -FN:67,RuleERC2980Internal._isWhitelisted -FNDA:89,RuleERC2980Internal._isWhitelisted -DA:68,89 -DA:71,5 -FN:71,RuleERC2980Internal._whitelistCount -FNDA:5,RuleERC2980Internal._whitelistCount -DA:72,5 -DA:79,4 -FN:79,RuleERC2980Internal._addFrozenlistAddresses +DA:92,4 +DA:105,4 +FN:105,RuleERC2980Internal._addFrozenlistAddresses FNDA:4,RuleERC2980Internal._addFrozenlistAddresses -DA:83,4 -DA:84,6 -BRDA:84,2,0,5 -BRDA:84,2,1,1 -DA:85,5 -DA:87,1 -DA:92,2 -FN:92,RuleERC2980Internal._removeFrozenlistAddresses -FNDA:2,RuleERC2980Internal._removeFrozenlistAddresses -DA:96,2 -DA:97,2 -BRDA:97,3,0,1 -BRDA:97,3,1,1 -DA:98,1 -DA:100,1 -DA:105,18 -FN:105,RuleERC2980Internal._addFrozenlistAddress -FNDA:18,RuleERC2980Internal._addFrozenlistAddress -DA:106,18 DA:109,4 -FN:109,RuleERC2980Internal._removeFrozenlistAddress +DA:112,6 +BRDA:112,3,0,- +BRDA:112,3,1,6 +DA:113,6 +BRDA:113,4,0,5 +BRDA:113,4,1,1 +DA:114,5 +DA:116,1 +DA:127,2 +FN:127,RuleERC2980Internal._removeFrozenlistAddresses +FNDA:2,RuleERC2980Internal._removeFrozenlistAddresses +DA:131,2 +DA:132,2 +BRDA:132,5,0,1 +BRDA:132,5,1,1 +DA:133,1 +DA:135,1 +DA:144,19 +FN:144,RuleERC2980Internal._addFrozenlistAddress +FNDA:19,RuleERC2980Internal._addFrozenlistAddress +DA:145,19 +DA:152,4 +FN:152,RuleERC2980Internal._removeFrozenlistAddress FNDA:4,RuleERC2980Internal._removeFrozenlistAddress -DA:110,4 -DA:113,94 -FN:113,RuleERC2980Internal._isFrozen -FNDA:94,RuleERC2980Internal._isFrozen -DA:114,94 -DA:117,4 -FN:117,RuleERC2980Internal._frozenlistCount +DA:153,4 +DA:165,115 +FN:165,RuleERC2980Internal._isWhitelisted +FNDA:115,RuleERC2980Internal._isWhitelisted +DA:166,115 +DA:173,5 +FN:173,RuleERC2980Internal._whitelistCount +FNDA:5,RuleERC2980Internal._whitelistCount +DA:174,5 +DA:182,163 +FN:182,RuleERC2980Internal._isFrozen +FNDA:163,RuleERC2980Internal._isFrozen +DA:183,163 +DA:190,4 +FN:190,RuleERC2980Internal._frozenlistCount FNDA:4,RuleERC2980Internal._frozenlistCount -DA:118,4 +DA:191,4 FNF:12 FNH:12 -LF:36 -LH:36 -BRF:8 -BRH:8 +LF:38 +LH:38 +BRF:12 +BRH:10 end_of_record TN: SF:src/rules/validation/abstract/base/RuleBlacklistBase.sol -DA:32,58 -FN:32,RuleBlacklistBase.transferred.0 -FNDA:58,RuleBlacklistBase.transferred.0 -DA:38,58 -DA:45,2 -FN:45,RuleBlacklistBase.transferred.1 -FNDA:2,RuleBlacklistBase.transferred.1 -DA:51,2 -DA:54,4 -FN:54,RuleBlacklistBase.canReturnTransferRestrictionCode +DA:37,18 +FN:37,RuleBlacklistBase.transferred.0 +FNDA:18,RuleBlacklistBase.transferred.0 +DA:43,18 +DA:50,46 +FN:50,RuleBlacklistBase.transferred.1 +FNDA:46,RuleBlacklistBase.transferred.1 +DA:56,46 +DA:62,4 +FN:62,RuleBlacklistBase.canReturnTransferRestrictionCode FNDA:4,RuleBlacklistBase.canReturnTransferRestrictionCode -DA:61,4 -DA:62,1 -DA:65,12 -FN:65,RuleBlacklistBase.messageForTransferRestriction +DA:69,4 +DA:70,1 +DA:76,12 +FN:76,RuleBlacklistBase.messageForTransferRestriction FNDA:12,RuleBlacklistBase.messageForTransferRestriction -DA:72,12 -BRDA:72,0,0,5 -BRDA:72,0,1,3 -DA:73,5 -DA:74,7 -BRDA:74,1,0,3 -BRDA:74,1,1,3 -DA:75,3 -DA:76,4 -BRDA:76,2,0,1 -BRDA:76,2,1,3 -DA:77,1 -DA:79,3 -DA:83,61 -FN:83,RuleBlacklistBase.supportsInterface -FNDA:61,RuleBlacklistBase.supportsInterface -DA:84,61 -DA:91,106 -FN:91,RuleBlacklistBase._detectTransferRestriction -FNDA:106,RuleBlacklistBase._detectTransferRestriction -DA:101,106 -BRDA:101,3,0,23 -BRDA:101,3,1,64 -DA:102,23 -DA:103,83 -BRDA:103,4,0,19 -DA:104,19 -DA:106,64 -DA:109,22 -FN:109,RuleBlacklistBase._detectTransferRestrictionFrom -FNDA:22,RuleBlacklistBase._detectTransferRestrictionFrom -DA:115,22 -BRDA:115,5,0,8 -DA:116,8 -DA:118,14 -DA:121,61 -FN:121,RuleBlacklistBase._transferred -FNDA:61,RuleBlacklistBase._transferred -DA:122,61 -DA:123,61 -BRDA:123,6,0,15 -BRDA:123,6,1,46 -DA:129,4 -FN:129,RuleBlacklistBase._transferredFrom -FNDA:4,RuleBlacklistBase._transferredFrom -DA:130,4 -DA:131,4 -BRDA:131,7,0,2 -BRDA:131,7,1,2 +DA:83,12 +BRDA:83,0,0,5 +BRDA:83,0,1,3 +DA:84,5 +DA:85,7 +BRDA:85,1,0,3 +BRDA:85,1,1,3 +DA:86,3 +DA:87,4 +BRDA:87,2,0,1 +BRDA:87,2,1,3 +DA:88,1 +DA:90,3 +DA:97,65 +FN:97,RuleBlacklistBase.supportsInterface +FNDA:65,RuleBlacklistBase.supportsInterface +DA:100,65 +DA:101,63 +DA:114,140 +FN:114,RuleBlacklistBase._detectTransferRestriction +FNDA:140,RuleBlacklistBase._detectTransferRestriction +DA:124,140 +BRDA:124,3,0,40 +BRDA:124,3,1,81 +DA:125,40 +DA:126,100 +BRDA:126,4,0,19 +DA:127,19 +DA:129,81 +DA:140,80 +FN:140,RuleBlacklistBase._detectTransferRestrictionFrom +FNDA:80,RuleBlacklistBase._detectTransferRestrictionFrom +DA:146,80 +BRDA:146,5,0,8 +DA:147,8 +DA:149,72 +DA:158,29 +FN:158,RuleBlacklistBase._transferred +FNDA:29,RuleBlacklistBase._transferred +DA:159,29 +DA:160,29 +BRDA:160,6,0,17 +BRDA:160,6,1,12 +DA:173,54 +FN:173,RuleBlacklistBase._transferredFrom +FNDA:54,RuleBlacklistBase._transferredFrom +DA:174,54 +DA:175,54 +BRDA:175,7,0,9 +BRDA:175,7,1,45 FNF:9 FNH:9 -LF:33 -LH:33 +LF:34 +LH:34 BRF:14 BRH:14 end_of_record TN: SF:src/rules/validation/abstract/base/RuleERC2980Base.sol -DA:41,67 -FN:41,RuleERC2980Base.constructor -FNDA:67,RuleERC2980Base.constructor -DA:42,2 -BRDA:42,0,0,2 -DA:43,2 -DA:44,2 -DA:52,6 -FN:52,RuleERC2980Base.onlyWhitelistAdd +DA:64,75 +FN:64,RuleERC2980Base.constructor +FNDA:75,RuleERC2980Base.constructor +DA:65,75 +DA:66,75 +DA:67,75 +DA:68,75 +DA:75,5 +FN:75,RuleERC2980Base.onlyMintBurnManager +FNDA:5,RuleERC2980Base.onlyMintBurnManager +DA:76,5 +DA:80,6 +FN:80,RuleERC2980Base.onlyWhitelistAdd FNDA:6,RuleERC2980Base.onlyWhitelistAdd -DA:53,6 -DA:57,7 -FN:57,RuleERC2980Base.onlyWhitelistRemove +DA:81,6 +DA:85,7 +FN:85,RuleERC2980Base.onlyWhitelistRemove FNDA:7,RuleERC2980Base.onlyWhitelistRemove -DA:58,7 -DA:62,6 -FN:62,RuleERC2980Base.onlyFrozenlistAdd +DA:86,7 +DA:90,6 +FN:90,RuleERC2980Base.onlyFrozenlistAdd FNDA:6,RuleERC2980Base.onlyFrozenlistAdd -DA:63,6 -DA:67,2 -FN:67,RuleERC2980Base.onlyFrozenlistRemove +DA:91,6 +DA:95,2 +FN:95,RuleERC2980Base.onlyFrozenlistRemove FNDA:2,RuleERC2980Base.onlyFrozenlistRemove -DA:68,2 -DA:72,0 -FN:72,RuleERC2980Base._authorizeWhitelistAdd -FNDA:0,RuleERC2980Base._authorizeWhitelistAdd -DA:73,0 -FN:73,RuleERC2980Base._authorizeWhitelistRemove -FNDA:0,RuleERC2980Base._authorizeWhitelistRemove -DA:74,0 -FN:74,RuleERC2980Base._authorizeFrozenlistAdd -FNDA:0,RuleERC2980Base._authorizeFrozenlistAdd -DA:75,0 -FN:75,RuleERC2980Base._authorizeFrozenlistRemove -FNDA:0,RuleERC2980Base._authorizeFrozenlistRemove -DA:85,6 -FN:85,RuleERC2980Base.addWhitelistAddresses +DA:96,2 +DA:109,6 +FN:109,RuleERC2980Base.addWhitelistAddresses FNDA:6,RuleERC2980Base.addWhitelistAddresses -DA:86,4 -DA:87,4 -DA:94,4 -FN:94,RuleERC2980Base.removeWhitelistAddresses +DA:110,4 +DA:111,4 +DA:119,4 +FN:119,RuleERC2980Base.removeWhitelistAddresses FNDA:4,RuleERC2980Base.removeWhitelistAddresses -DA:95,3 -DA:96,3 -DA:107,45 -FN:107,RuleERC2980Base.addWhitelistAddress -FNDA:45,RuleERC2980Base.addWhitelistAddress -DA:108,42 -BRDA:108,1,0,1 -BRDA:108,1,1,41 -DA:109,41 -DA:110,41 -DA:121,7 -FN:121,RuleERC2980Base.removeWhitelistAddress +DA:120,3 +DA:121,3 +DA:133,49 +FN:133,RuleERC2980Base.addWhitelistAddress +FNDA:49,RuleERC2980Base.addWhitelistAddress +DA:134,46 +BRDA:134,0,0,1 +BRDA:134,0,1,45 +DA:135,45 +BRDA:135,1,0,1 +BRDA:135,1,1,44 +DA:136,44 +DA:137,44 +DA:149,7 +FN:149,RuleERC2980Base.removeWhitelistAddress FNDA:7,RuleERC2980Base.removeWhitelistAddress -DA:122,5 -BRDA:122,2,0,1 -BRDA:122,2,1,4 -DA:123,4 -DA:124,4 -DA:135,6 -FN:135,RuleERC2980Base.addFrozenlistAddresses +DA:150,5 +BRDA:150,2,0,1 +BRDA:150,2,1,4 +DA:151,4 +DA:152,4 +DA:164,6 +FN:164,RuleERC2980Base.addFrozenlistAddresses FNDA:6,RuleERC2980Base.addFrozenlistAddresses -DA:136,4 -DA:137,4 -DA:144,2 -FN:144,RuleERC2980Base.removeFrozenlistAddresses +DA:165,4 +DA:166,4 +DA:174,2 +FN:174,RuleERC2980Base.removeFrozenlistAddresses FNDA:2,RuleERC2980Base.removeFrozenlistAddresses -DA:145,2 -DA:146,2 -DA:157,22 -FN:157,RuleERC2980Base.addFrozenlistAddress -FNDA:22,RuleERC2980Base.addFrozenlistAddress -DA:158,19 -BRDA:158,3,0,1 -BRDA:158,3,1,18 -DA:159,18 -DA:160,18 -DA:171,7 -FN:171,RuleERC2980Base.removeFrozenlistAddress +DA:175,2 +DA:176,2 +DA:188,24 +FN:188,RuleERC2980Base.addFrozenlistAddress +FNDA:24,RuleERC2980Base.addFrozenlistAddress +DA:189,21 +BRDA:189,3,0,1 +BRDA:189,3,1,20 +DA:190,20 +BRDA:190,4,0,1 +BRDA:190,4,1,19 +DA:191,19 +DA:192,19 +DA:204,7 +FN:204,RuleERC2980Base.removeFrozenlistAddress FNDA:7,RuleERC2980Base.removeFrozenlistAddress -DA:172,5 -BRDA:172,4,0,1 -BRDA:172,4,1,4 -DA:173,4 -DA:174,4 -DA:181,4 -FN:181,RuleERC2980Base.transferred.0 -FNDA:4,RuleERC2980Base.transferred.0 -DA:187,4 -DA:190,2 -FN:190,RuleERC2980Base.transferred.1 -FNDA:2,RuleERC2980Base.transferred.1 -DA:196,2 -DA:199,5 -FN:199,RuleERC2980Base.canReturnTransferRestrictionCode +DA:205,5 +BRDA:205,5,0,1 +BRDA:205,5,1,4 +DA:206,4 +DA:207,4 +DA:218,5 +FN:218,RuleERC2980Base.setAllowMint +FNDA:5,RuleERC2980Base.setAllowMint +DA:219,3 +DA:220,3 +DA:227,3 +FN:227,RuleERC2980Base.setAllowBurn +FNDA:3,RuleERC2980Base.setAllowBurn +DA:228,2 +DA:229,2 +DA:235,6 +FN:235,RuleERC2980Base.transferred.0 +FNDA:6,RuleERC2980Base.transferred.0 +DA:241,6 +DA:247,4 +FN:247,RuleERC2980Base.transferred.1 +FNDA:4,RuleERC2980Base.transferred.1 +DA:253,4 +DA:259,5 +FN:259,RuleERC2980Base.canReturnTransferRestrictionCode FNDA:5,RuleERC2980Base.canReturnTransferRestrictionCode -DA:206,5 -DA:207,3 -DA:210,5 -FN:210,RuleERC2980Base.messageForTransferRestriction -FNDA:5,RuleERC2980Base.messageForTransferRestriction -DA:217,5 -BRDA:217,5,0,1 -BRDA:217,5,1,1 -DA:218,1 -DA:219,4 -BRDA:219,6,0,1 -BRDA:219,6,1,1 -DA:220,1 -DA:221,3 -BRDA:221,7,0,1 -BRDA:221,7,1,1 -DA:222,1 -DA:223,2 -BRDA:223,8,0,1 -BRDA:223,8,1,1 -DA:224,1 -DA:226,1 -DA:230,1 -FN:230,RuleERC2980Base.supportsInterface -FNDA:1,RuleERC2980Base.supportsInterface -DA:231,1 -DA:237,5 -FN:237,RuleERC2980Base.whitelistAddressCount +DA:266,5 +DA:267,3 +DA:268,1 +DA:274,7 +FN:274,RuleERC2980Base.messageForTransferRestriction +FNDA:7,RuleERC2980Base.messageForTransferRestriction +DA:281,7 +BRDA:281,6,0,1 +BRDA:281,6,1,1 +DA:282,1 +DA:283,6 +BRDA:283,7,0,1 +BRDA:283,7,1,1 +DA:284,1 +DA:285,5 +BRDA:285,8,0,1 +BRDA:285,8,1,1 +DA:286,1 +DA:287,4 +BRDA:287,9,0,1 +BRDA:287,9,1,1 +DA:288,1 +DA:289,3 +BRDA:289,10,0,1 +BRDA:289,10,1,1 +DA:290,1 +DA:291,2 +BRDA:291,11,0,1 +BRDA:291,11,1,1 +DA:292,1 +DA:294,1 +DA:301,3 +FN:301,RuleERC2980Base.supportsInterface +FNDA:3,RuleERC2980Base.supportsInterface +DA:302,3 +DA:309,5 +FN:309,RuleERC2980Base.whitelistAddressCount FNDA:5,RuleERC2980Base.whitelistAddressCount -DA:238,5 -DA:244,14 -FN:244,RuleERC2980Base.isWhitelisted -FNDA:14,RuleERC2980Base.isWhitelisted -DA:245,14 -DA:251,7 -FN:251,RuleERC2980Base.whitelist -FNDA:7,RuleERC2980Base.whitelist -DA:252,7 -DA:260,3 -FN:260,RuleERC2980Base.isVerified -FNDA:3,RuleERC2980Base.isVerified -DA:261,3 -DA:267,1 -FN:267,RuleERC2980Base.areWhitelisted +DA:310,5 +DA:318,15 +FN:318,RuleERC2980Base.isWhitelisted +FNDA:15,RuleERC2980Base.isWhitelisted +DA:319,15 +DA:327,11 +FN:327,RuleERC2980Base.whitelist +FNDA:11,RuleERC2980Base.whitelist +DA:328,11 +DA:338,5 +FN:338,RuleERC2980Base.isVerified +FNDA:5,RuleERC2980Base.isVerified +DA:339,5 +DA:347,1 +FN:347,RuleERC2980Base.areWhitelisted FNDA:1,RuleERC2980Base.areWhitelisted -DA:268,1 -DA:269,1 -DA:270,2 -DA:277,4 -FN:277,RuleERC2980Base.frozenlistAddressCount +DA:348,1 +DA:349,1 +DA:350,2 +DA:358,4 +FN:358,RuleERC2980Base.frozenlistAddressCount FNDA:4,RuleERC2980Base.frozenlistAddressCount -DA:278,4 -DA:284,12 -FN:284,RuleERC2980Base.isFrozen +DA:359,4 +DA:367,12 +FN:367,RuleERC2980Base.isFrozen FNDA:12,RuleERC2980Base.isFrozen -DA:285,12 -DA:291,4 -FN:291,RuleERC2980Base.frozenlist -FNDA:4,RuleERC2980Base.frozenlist -DA:292,4 -DA:298,1 -FN:298,RuleERC2980Base.areFrozen +DA:368,12 +DA:376,7 +FN:376,RuleERC2980Base.frozenlist +FNDA:7,RuleERC2980Base.frozenlist +DA:377,7 +DA:385,1 +FN:385,RuleERC2980Base.areFrozen FNDA:1,RuleERC2980Base.areFrozen -DA:299,1 -DA:300,1 -DA:301,2 -DA:309,24 -FN:309,RuleERC2980Base._detectTransferRestriction -FNDA:24,RuleERC2980Base._detectTransferRestriction -DA:321,24 -BRDA:321,9,0,4 -BRDA:321,9,1,16 -DA:322,4 -DA:323,20 -BRDA:323,10,0,4 -DA:324,4 -DA:327,16 -BRDA:327,11,0,5 -DA:328,5 -DA:330,11 -DA:333,8 -FN:333,RuleERC2980Base._detectTransferRestrictionFrom -FNDA:8,RuleERC2980Base._detectTransferRestrictionFrom -DA:340,8 -BRDA:340,12,0,4 -DA:341,4 -DA:343,4 -DA:346,5 -FN:346,RuleERC2980Base._transferred -FNDA:5,RuleERC2980Base._transferred -DA:347,5 -DA:348,5 -BRDA:348,13,0,3 -BRDA:348,13,1,2 -DA:354,3 -FN:354,RuleERC2980Base._transferredFrom -FNDA:3,RuleERC2980Base._transferredFrom -DA:355,3 -DA:356,3 -BRDA:356,14,0,1 -BRDA:356,14,1,2 -DA:362,272 -FN:362,RuleERC2980Base._msgSender -FNDA:272,RuleERC2980Base._msgSender -DA:363,272 -DA:366,2 -FN:366,RuleERC2980Base._msgData +DA:386,1 +DA:387,1 +DA:388,2 +DA:399,0 +FN:399,RuleERC2980Base._authorizeMintBurnManager +FNDA:0,RuleERC2980Base._authorizeMintBurnManager +DA:404,0 +FN:404,RuleERC2980Base._authorizeWhitelistAdd +FNDA:0,RuleERC2980Base._authorizeWhitelistAdd +DA:408,0 +FN:408,RuleERC2980Base._authorizeWhitelistRemove +FNDA:0,RuleERC2980Base._authorizeWhitelistRemove +DA:412,0 +FN:412,RuleERC2980Base._authorizeFrozenlistAdd +FNDA:0,RuleERC2980Base._authorizeFrozenlistAdd +DA:416,0 +FN:416,RuleERC2980Base._authorizeFrozenlistRemove +FNDA:0,RuleERC2980Base._authorizeFrozenlistRemove +DA:421,62 +FN:421,RuleERC2980Base._detectTransferRestriction +FNDA:62,RuleERC2980Base._detectTransferRestriction +DA:432,62 +DA:433,62 +DA:436,62 +BRDA:436,12,0,1 +DA:437,1 +DA:439,61 +BRDA:439,13,0,2 +DA:440,2 +DA:444,59 +BRDA:444,14,0,20 +DA:445,20 +DA:447,39 +BRDA:447,15,0,4 +DA:448,4 +DA:451,35 +BRDA:451,16,0,5 +DA:452,5 +DA:454,30 +DA:460,24 +FN:460,RuleERC2980Base._detectTransferRestrictionFrom +FNDA:24,RuleERC2980Base._detectTransferRestrictionFrom +DA:467,24 +BRDA:467,17,0,4 +DA:468,4 +DA:470,20 +DA:476,13 +FN:476,RuleERC2980Base._transferred +FNDA:13,RuleERC2980Base._transferred +DA:477,13 +DA:478,13 +BRDA:478,18,0,7 +BRDA:478,18,1,6 +DA:487,11 +FN:487,RuleERC2980Base._transferredFrom +FNDA:11,RuleERC2980Base._transferredFrom +DA:488,11 +DA:489,11 +BRDA:489,19,0,5 +BRDA:489,19,1,6 +DA:498,293 +FN:498,RuleERC2980Base._msgSender +FNDA:293,RuleERC2980Base._msgSender +DA:499,293 +DA:505,2 +FN:505,RuleERC2980Base._msgData FNDA:2,RuleERC2980Base._msgData -DA:367,2 -DA:370,274 -FN:370,RuleERC2980Base._contextSuffixLength -FNDA:274,RuleERC2980Base._contextSuffixLength -DA:371,274 -FNF:38 -FNH:34 -LF:109 -LH:105 -BRF:26 -BRH:26 +DA:506,2 +DA:512,295 +FN:512,RuleERC2980Base._contextSuffixLength +FNDA:295,RuleERC2980Base._contextSuffixLength +DA:513,295 +FNF:42 +FNH:37 +LF:132 +LH:127 +BRF:34 +BRH:34 end_of_record TN: SF:src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol -DA:23,26 -FN:23,RuleIdentityRegistryBase.constructor -FNDA:26,RuleIdentityRegistryBase.constructor -DA:24,26 -BRDA:24,0,0,25 -DA:25,25 -DA:33,5 -FN:33,RuleIdentityRegistryBase.onlyIdentityRegistryManager +DA:63,39 +FN:63,RuleIdentityRegistryBase.constructor +FNDA:39,RuleIdentityRegistryBase.constructor +DA:64,39 +BRDA:64,0,0,37 +DA:65,37 +DA:67,39 +DA:68,39 +DA:69,39 +DA:70,39 +DA:77,5 +FN:77,RuleIdentityRegistryBase.onlyIdentityRegistryManager FNDA:5,RuleIdentityRegistryBase.onlyIdentityRegistryManager -DA:34,5 -DA:38,0 -FN:38,RuleIdentityRegistryBase._authorizeIdentityRegistryManager -FNDA:0,RuleIdentityRegistryBase._authorizeIdentityRegistryManager -DA:44,4 -FN:44,RuleIdentityRegistryBase.canReturnTransferRestrictionCode +DA:78,5 +DA:91,4 +FN:91,RuleIdentityRegistryBase.canReturnTransferRestrictionCode FNDA:4,RuleIdentityRegistryBase.canReturnTransferRestrictionCode -DA:45,4 -DA:46,2 -DA:53,4 -FN:53,RuleIdentityRegistryBase.setIdentityRegistry +DA:92,4 +DA:93,2 +DA:104,4 +FN:104,RuleIdentityRegistryBase.setIdentityRegistry FNDA:4,RuleIdentityRegistryBase.setIdentityRegistry -DA:54,2 -BRDA:54,1,0,1 -BRDA:54,1,1,1 -DA:55,1 -DA:56,1 -DA:59,5 -FN:59,RuleIdentityRegistryBase.clearIdentityRegistry +DA:105,2 +BRDA:105,1,0,1 +BRDA:105,1,1,1 +DA:106,1 +DA:107,1 +DA:116,2 +FN:116,RuleIdentityRegistryBase.setCheckSender +FNDA:2,RuleIdentityRegistryBase.setCheckSender +DA:117,2 +DA:118,2 +DA:126,5 +FN:126,RuleIdentityRegistryBase.setCheckSpender +FNDA:5,RuleIdentityRegistryBase.setCheckSpender +DA:127,5 +DA:128,5 +DA:134,5 +FN:134,RuleIdentityRegistryBase.clearIdentityRegistry FNDA:5,RuleIdentityRegistryBase.clearIdentityRegistry -DA:60,3 -DA:61,3 -DA:64,2 -FN:64,RuleIdentityRegistryBase.transferred.0 -FNDA:2,RuleIdentityRegistryBase.transferred.0 -DA:65,2 -DA:68,2 -FN:68,RuleIdentityRegistryBase.transferred.1 -FNDA:2,RuleIdentityRegistryBase.transferred.1 -DA:69,2 -DA:72,4 -FN:72,RuleIdentityRegistryBase.messageForTransferRestriction +DA:135,3 +DA:136,3 +DA:142,5 +FN:142,RuleIdentityRegistryBase.transferred.0 +FNDA:5,RuleIdentityRegistryBase.transferred.0 +DA:143,5 +DA:149,7 +FN:149,RuleIdentityRegistryBase.transferred.1 +FNDA:7,RuleIdentityRegistryBase.transferred.1 +DA:150,7 +DA:156,4 +FN:156,RuleIdentityRegistryBase.messageForTransferRestriction FNDA:4,RuleIdentityRegistryBase.messageForTransferRestriction -DA:78,4 -BRDA:78,2,0,1 -BRDA:78,2,1,1 -DA:79,1 -DA:80,3 -BRDA:80,3,0,1 -BRDA:80,3,1,1 -DA:81,1 -DA:82,2 -BRDA:82,4,0,1 -DA:83,1 -DA:85,1 -DA:92,16 -FN:92,RuleIdentityRegistryBase._detectTransferRestriction -FNDA:16,RuleIdentityRegistryBase._detectTransferRestriction -DA:102,16 -BRDA:102,5,0,3 -DA:103,3 -DA:105,13 -BRDA:105,6,0,2 -DA:106,2 -DA:109,11 -BRDA:109,7,0,4 -DA:110,4 -DA:112,7 -BRDA:112,8,0,1 -DA:113,1 -DA:115,6 -DA:118,9 -FN:118,RuleIdentityRegistryBase._detectTransferRestrictionFrom -FNDA:9,RuleIdentityRegistryBase._detectTransferRestrictionFrom -DA:124,9 -BRDA:124,9,0,1 -DA:125,1 -DA:127,8 -BRDA:127,10,0,1 -DA:128,1 -DA:130,7 -BRDA:130,11,0,4 -DA:131,4 -DA:133,3 -DA:136,2 -FN:136,RuleIdentityRegistryBase._transferred -FNDA:2,RuleIdentityRegistryBase._transferred -DA:137,2 -DA:138,2 -BRDA:138,12,0,1 -BRDA:138,12,1,1 -DA:144,2 -FN:144,RuleIdentityRegistryBase._transferredFrom -FNDA:2,RuleIdentityRegistryBase._transferredFrom -DA:145,2 -DA:146,2 -BRDA:146,13,0,1 -BRDA:146,13,1,1 -FNF:13 -FNH:12 -LF:52 -LH:51 +DA:162,4 +BRDA:162,2,0,1 +BRDA:162,2,1,1 +DA:163,1 +DA:164,3 +BRDA:164,3,0,1 +BRDA:164,3,1,1 +DA:165,1 +DA:166,2 +BRDA:166,4,0,1 +DA:167,1 +DA:169,1 +DA:179,0 +FN:179,RuleIdentityRegistryBase._authorizeIdentityRegistryManager +FNDA:0,RuleIdentityRegistryBase._authorizeIdentityRegistryManager +DA:187,62 +FN:187,RuleIdentityRegistryBase._detectTransferRestriction +FNDA:62,RuleIdentityRegistryBase._detectTransferRestriction +DA:197,62 +BRDA:197,5,0,3 +DA:198,3 +DA:201,59 +BRDA:201,6,0,3 +DA:202,3 +DA:206,56 +BRDA:206,7,0,1 +DA:207,1 +DA:212,55 +BRDA:212,8,0,6 +DA:213,6 +DA:215,49 +DA:226,31 +FN:226,RuleIdentityRegistryBase._detectTransferRestrictionFrom +FNDA:31,RuleIdentityRegistryBase._detectTransferRestrictionFrom +DA:232,31 +BRDA:232,9,0,1 +DA:233,1 +DA:236,30 +BRDA:236,10,0,2 +DA:237,2 +DA:245,5 +DA:246,4 +DA:247,3 +BRDA:247,11,0,3 +DA:248,3 +DA:250,25 +DA:256,11 +FN:256,RuleIdentityRegistryBase._transferred +FNDA:11,RuleIdentityRegistryBase._transferred +DA:257,11 +DA:258,11 +BRDA:258,12,0,1 +BRDA:258,12,1,10 +DA:267,13 +FN:267,RuleIdentityRegistryBase._transferredFrom +FNDA:13,RuleIdentityRegistryBase._transferredFrom +DA:268,13 +DA:269,13 +BRDA:269,13,0,2 +BRDA:269,13,1,11 +FNF:15 +FNH:14 +LF:64 +LH:63 BRF:19 BRH:19 end_of_record TN: SF:src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol -DA:24,25 -FN:24,RuleMaxTotalSupplyBase.constructor -FNDA:25,RuleMaxTotalSupplyBase.constructor -DA:25,25 -BRDA:25,0,0,1 -BRDA:25,0,1,24 -DA:26,24 -DA:27,24 -DA:34,2 -FN:34,RuleMaxTotalSupplyBase.canReturnTransferRestrictionCode +DA:34,542 +FN:34,RuleMaxTotalSupplyBase.constructor +FNDA:542,RuleMaxTotalSupplyBase.constructor +DA:35,542 +BRDA:35,0,0,1 +BRDA:35,0,1,541 +DA:36,541 +DA:37,541 +DA:49,2 +FN:49,RuleMaxTotalSupplyBase.canReturnTransferRestrictionCode FNDA:2,RuleMaxTotalSupplyBase.canReturnTransferRestrictionCode -DA:35,2 -DA:42,260 -FN:42,RuleMaxTotalSupplyBase.setMaxTotalSupply +DA:50,2 +DA:61,260 +FN:61,RuleMaxTotalSupplyBase.setMaxTotalSupply FNDA:260,RuleMaxTotalSupplyBase.setMaxTotalSupply -DA:43,258 -DA:44,258 -DA:47,4 -FN:47,RuleMaxTotalSupplyBase.setTokenContract +DA:62,258 +DA:63,258 +DA:70,4 +FN:70,RuleMaxTotalSupplyBase.setTokenContract FNDA:4,RuleMaxTotalSupplyBase.setTokenContract -DA:48,2 -BRDA:48,1,0,1 -BRDA:48,1,1,1 -DA:49,1 -DA:50,1 -DA:53,2 -FN:53,RuleMaxTotalSupplyBase.transferred.0 +DA:71,2 +BRDA:71,1,0,1 +BRDA:71,1,1,1 +DA:72,1 +DA:73,1 +DA:79,2 +FN:79,RuleMaxTotalSupplyBase.transferred.0 FNDA:2,RuleMaxTotalSupplyBase.transferred.0 -DA:54,2 -DA:57,2 -FN:57,RuleMaxTotalSupplyBase.transferred.1 +DA:80,2 +DA:86,2 +FN:86,RuleMaxTotalSupplyBase.transferred.1 FNDA:2,RuleMaxTotalSupplyBase.transferred.1 -DA:58,2 -DA:61,2 -FN:61,RuleMaxTotalSupplyBase.messageForTransferRestriction +DA:87,2 +DA:93,2 +FN:93,RuleMaxTotalSupplyBase.messageForTransferRestriction FNDA:2,RuleMaxTotalSupplyBase.messageForTransferRestriction -DA:67,2 -BRDA:67,2,0,1 -DA:68,1 -DA:70,1 -DA:77,260 -FN:77,RuleMaxTotalSupplyBase.onlyMaxTotalSupplyManager +DA:99,2 +BRDA:99,2,0,1 +DA:100,1 +DA:102,1 +DA:109,260 +FN:109,RuleMaxTotalSupplyBase.onlyMaxTotalSupplyManager FNDA:260,RuleMaxTotalSupplyBase.onlyMaxTotalSupplyManager -DA:78,260 -DA:82,0 -FN:82,RuleMaxTotalSupplyBase._authorizeMaxTotalSupplyManager +DA:110,260 +DA:117,0 +FN:117,RuleMaxTotalSupplyBase._authorizeMaxTotalSupplyManager FNDA:0,RuleMaxTotalSupplyBase._authorizeMaxTotalSupplyManager -DA:88,271 -FN:88,RuleMaxTotalSupplyBase._detectTransferRestriction -FNDA:271,RuleMaxTotalSupplyBase._detectTransferRestriction -DA:99,271 -BRDA:99,3,0,268 -DA:100,268 -DA:101,268 -BRDA:101,4,0,186 -DA:102,186 -DA:105,85 -DA:108,2 -FN:108,RuleMaxTotalSupplyBase._detectTransferRestrictionFrom -FNDA:2,RuleMaxTotalSupplyBase._detectTransferRestrictionFrom -DA:114,2 -DA:117,2 -FN:117,RuleMaxTotalSupplyBase._transferred +DA:126,787 +FN:126,RuleMaxTotalSupplyBase._detectTransferRestriction +FNDA:787,RuleMaxTotalSupplyBase._detectTransferRestriction +DA:137,787 +BRDA:137,3,0,784 +DA:138,784 +DA:141,784 +BRDA:141,4,0,458 +DA:142,458 +DA:145,329 +DA:151,3 +FN:151,RuleMaxTotalSupplyBase._detectTransferRestrictionFrom +FNDA:3,RuleMaxTotalSupplyBase._detectTransferRestrictionFrom +DA:157,3 +DA:166,2 +FN:166,RuleMaxTotalSupplyBase._transferred FNDA:2,RuleMaxTotalSupplyBase._transferred -DA:118,2 -DA:119,2 -BRDA:119,5,0,1 -BRDA:119,5,1,1 -DA:125,2 -FN:125,RuleMaxTotalSupplyBase._transferredFrom +DA:167,2 +DA:168,2 +BRDA:168,5,0,1 +BRDA:168,5,1,1 +DA:181,2 +FN:181,RuleMaxTotalSupplyBase._transferredFrom FNDA:2,RuleMaxTotalSupplyBase._transferredFrom -DA:126,2 -DA:127,2 -BRDA:127,6,0,1 -BRDA:127,6,1,1 +DA:182,2 +DA:183,2 +BRDA:183,6,0,1 +BRDA:183,6,1,1 FNF:13 FNH:12 LF:38 @@ -927,100 +1439,100 @@ BRH:11 end_of_record TN: SF:src/rules/validation/abstract/base/RuleSanctionsListBase.sol -DA:24,46 -FN:24,RuleSanctionsListBase.constructor -FNDA:46,RuleSanctionsListBase.constructor -DA:27,45 -BRDA:27,0,0,20 -DA:28,20 -DA:36,3 -FN:36,RuleSanctionsListBase.canReturnTransferRestrictionCode +DA:32,49 +FN:32,RuleSanctionsListBase.constructor +FNDA:49,RuleSanctionsListBase.constructor +DA:35,48 +BRDA:35,0,0,21 +DA:36,21 +DA:47,3 +FN:47,RuleSanctionsListBase.canReturnTransferRestrictionCode FNDA:3,RuleSanctionsListBase.canReturnTransferRestrictionCode -DA:37,3 -DA:38,1 -DA:45,17 -FN:45,RuleSanctionsListBase.setSanctionListOracle -FNDA:17,RuleSanctionsListBase.setSanctionListOracle -DA:46,15 -BRDA:46,1,0,1 -BRDA:46,1,1,14 -DA:47,14 -DA:50,3 -FN:50,RuleSanctionsListBase.clearSanctionListOracle +DA:48,3 +DA:49,1 +DA:61,18 +FN:61,RuleSanctionsListBase.setSanctionListOracle +FNDA:18,RuleSanctionsListBase.setSanctionListOracle +DA:62,16 +BRDA:62,1,0,1 +BRDA:62,1,1,15 +DA:63,15 +DA:70,3 +FN:70,RuleSanctionsListBase.clearSanctionListOracle FNDA:3,RuleSanctionsListBase.clearSanctionListOracle -DA:51,3 -DA:54,43 -FN:54,RuleSanctionsListBase.transferred.0 -FNDA:43,RuleSanctionsListBase.transferred.0 -DA:55,43 -DA:58,2 -FN:58,RuleSanctionsListBase.transferred.1 -FNDA:2,RuleSanctionsListBase.transferred.1 -DA:59,2 -DA:62,4 -FN:62,RuleSanctionsListBase.messageForTransferRestriction +DA:71,3 +DA:77,9 +FN:77,RuleSanctionsListBase.transferred.0 +FNDA:9,RuleSanctionsListBase.transferred.0 +DA:78,9 +DA:84,41 +FN:84,RuleSanctionsListBase.transferred.1 +FNDA:41,RuleSanctionsListBase.transferred.1 +DA:85,41 +DA:91,4 +FN:91,RuleSanctionsListBase.messageForTransferRestriction FNDA:4,RuleSanctionsListBase.messageForTransferRestriction -DA:68,4 -BRDA:68,2,0,1 -BRDA:68,2,1,1 -DA:69,1 -DA:70,3 -BRDA:70,3,0,1 -BRDA:70,3,1,1 -DA:71,1 -DA:72,2 -BRDA:72,4,0,1 -DA:73,1 -DA:75,1 -DA:82,3 -FN:82,RuleSanctionsListBase.onlySanctionListManager +DA:97,4 +BRDA:97,2,0,1 +BRDA:97,2,1,1 +DA:98,1 +DA:99,3 +BRDA:99,3,0,1 +BRDA:99,3,1,1 +DA:100,1 +DA:101,2 +BRDA:101,4,0,1 +DA:102,1 +DA:104,1 +DA:111,3 +FN:111,RuleSanctionsListBase.onlySanctionListManager FNDA:3,RuleSanctionsListBase.onlySanctionListManager -DA:83,3 -DA:87,0 -FN:87,RuleSanctionsListBase._authorizeSanctionListManager +DA:112,3 +DA:124,39 +FN:124,RuleSanctionsListBase._setSanctionListOracle +FNDA:39,RuleSanctionsListBase._setSanctionListOracle +DA:125,39 +DA:126,39 +DA:133,0 +FN:133,RuleSanctionsListBase._authorizeSanctionListManager FNDA:0,RuleSanctionsListBase._authorizeSanctionListManager -DA:93,84 -FN:93,RuleSanctionsListBase._detectTransferRestriction -FNDA:84,RuleSanctionsListBase._detectTransferRestriction -DA:103,84 -BRDA:103,5,0,77 -DA:104,77 -BRDA:104,6,0,10 -BRDA:104,6,1,55 -DA:105,10 -DA:106,67 -BRDA:106,7,0,12 -DA:107,12 -DA:110,62 -DA:113,16 -FN:113,RuleSanctionsListBase._detectTransferRestrictionFrom -FNDA:16,RuleSanctionsListBase._detectTransferRestrictionFrom -DA:120,16 -BRDA:120,8,0,15 -DA:121,15 -BRDA:121,9,0,6 -DA:122,6 -DA:124,9 -DA:126,1 -DA:129,47 -FN:129,RuleSanctionsListBase._transferred -FNDA:47,RuleSanctionsListBase._transferred -DA:130,47 -DA:131,47 -BRDA:131,10,0,6 -BRDA:131,10,1,41 -DA:137,3 -FN:137,RuleSanctionsListBase._transferredFrom -FNDA:3,RuleSanctionsListBase._transferredFrom -DA:138,3 -DA:139,3 -BRDA:139,11,0,2 -BRDA:139,11,1,1 -DA:145,37 -FN:145,RuleSanctionsListBase._setSanctionListOracle -FNDA:37,RuleSanctionsListBase._setSanctionListOracle -DA:146,37 -DA:147,37 +DA:141,119 +FN:141,RuleSanctionsListBase._detectTransferRestriction +FNDA:119,RuleSanctionsListBase._detectTransferRestriction +DA:151,119 +BRDA:151,5,0,112 +DA:152,112 +BRDA:152,6,0,27 +BRDA:152,6,1,73 +DA:153,27 +DA:154,85 +BRDA:154,7,0,12 +DA:155,12 +DA:158,80 +DA:169,69 +FN:169,RuleSanctionsListBase._detectTransferRestrictionFrom +FNDA:69,RuleSanctionsListBase._detectTransferRestrictionFrom +DA:176,69 +BRDA:176,8,0,68 +DA:177,68 +BRDA:177,9,0,6 +DA:178,6 +DA:180,62 +DA:182,1 +DA:191,19 +FN:191,RuleSanctionsListBase._transferred +FNDA:19,RuleSanctionsListBase._transferred +DA:192,19 +DA:193,19 +BRDA:193,10,0,10 +BRDA:193,10,1,9 +DA:206,48 +FN:206,RuleSanctionsListBase._transferredFrom +FNDA:48,RuleSanctionsListBase._transferredFrom +DA:207,48 +DA:208,48 +BRDA:208,11,0,6 +BRDA:208,11,1,42 FNF:14 FNH:13 LF:48 @@ -1030,279 +1542,315 @@ BRH:18 end_of_record TN: SF:src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol -DA:27,2 -FN:27,RuleSpenderWhitelistBase.canReturnTransferRestrictionCode +DA:38,2 +FN:38,RuleSpenderWhitelistBase.canReturnTransferRestrictionCode FNDA:2,RuleSpenderWhitelistBase.canReturnTransferRestrictionCode -DA:28,2 -DA:38,1 -FN:38,RuleSpenderWhitelistBase.transferred.0 -FNDA:1,RuleSpenderWhitelistBase.transferred.0 -DA:40,2 -FN:40,RuleSpenderWhitelistBase.transferred.1 -FNDA:2,RuleSpenderWhitelistBase.transferred.1 -DA:41,2 -DA:44,2 -FN:44,RuleSpenderWhitelistBase.messageForTransferRestriction +DA:39,2 +DA:49,3 +FN:49,RuleSpenderWhitelistBase.transferred.0 +FNDA:3,RuleSpenderWhitelistBase.transferred.0 +DA:54,6 +FN:54,RuleSpenderWhitelistBase.transferred.1 +FNDA:6,RuleSpenderWhitelistBase.transferred.1 +DA:55,6 +DA:61,2 +FN:61,RuleSpenderWhitelistBase.messageForTransferRestriction FNDA:2,RuleSpenderWhitelistBase.messageForTransferRestriction -DA:50,2 -BRDA:50,0,0,1 -DA:51,1 -DA:53,1 -DA:60,4 -FN:60,RuleSpenderWhitelistBase._detectTransferRestriction -FNDA:4,RuleSpenderWhitelistBase._detectTransferRestriction -DA:61,4 -DA:64,13 -FN:64,RuleSpenderWhitelistBase._detectTransferRestrictionFrom -FNDA:13,RuleSpenderWhitelistBase._detectTransferRestrictionFrom -DA:71,13 -BRDA:71,1,0,5 -DA:72,5 -DA:74,8 -DA:77,3 -FN:77,RuleSpenderWhitelistBase._transferred -FNDA:3,RuleSpenderWhitelistBase._transferred -DA:81,7 -FN:81,RuleSpenderWhitelistBase._transferredFrom -FNDA:7,RuleSpenderWhitelistBase._transferredFrom -DA:82,7 -DA:83,7 -BRDA:83,2,0,3 -BRDA:83,2,1,4 -FNF:8 -FNH:8 -LF:19 -LH:19 +DA:67,2 +BRDA:67,0,0,1 +DA:68,1 +DA:70,1 +DA:76,8 +FN:76,RuleSpenderWhitelistBase.supportsInterface +FNDA:8,RuleSpenderWhitelistBase.supportsInterface +DA:79,8 +DA:80,6 +DA:91,12 +FN:91,RuleSpenderWhitelistBase._detectTransferRestriction +FNDA:12,RuleSpenderWhitelistBase._detectTransferRestriction +DA:92,12 +DA:102,35 +FN:102,RuleSpenderWhitelistBase._detectTransferRestrictionFrom +FNDA:35,RuleSpenderWhitelistBase._detectTransferRestrictionFrom +DA:111,35 +BRDA:111,1,0,13 +DA:112,13 +DA:114,22 +DA:120,9 +FN:120,RuleSpenderWhitelistBase._transferred +FNDA:9,RuleSpenderWhitelistBase._transferred +DA:131,17 +FN:131,RuleSpenderWhitelistBase._transferredFrom +FNDA:17,RuleSpenderWhitelistBase._transferredFrom +DA:132,17 +DA:133,17 +BRDA:133,2,0,7 +BRDA:133,2,1,10 +FNF:9 +FNH:9 +LF:22 +LH:22 BRF:4 BRH:4 end_of_record TN: SF:src/rules/validation/abstract/base/RuleWhitelistBase.sol -DA:18,163 -FN:18,RuleWhitelistBase.constructor -FNDA:163,RuleWhitelistBase.constructor -DA:21,163 -DA:22,2 -BRDA:22,0,0,2 -DA:23,2 -DA:24,2 -DA:32,3 -FN:32,RuleWhitelistBase.setCheckSpender +DA:32,188 +FN:32,RuleWhitelistBase.constructor +FNDA:188,RuleWhitelistBase.constructor +DA:35,188 +DA:36,188 +DA:48,3 +FN:48,RuleWhitelistBase.setCheckSpender FNDA:3,RuleWhitelistBase.setCheckSpender -DA:33,2 -DA:34,2 -DA:37,2 -FN:37,RuleWhitelistBase.isVerified -FNDA:2,RuleWhitelistBase.isVerified -DA:44,2 -DA:47,27 -FN:47,RuleWhitelistBase.supportsInterface -FNDA:27,RuleWhitelistBase.supportsInterface -DA:48,27 -DA:55,3 -FN:55,RuleWhitelistBase.onlyCheckSpenderManager +DA:49,2 +DA:50,2 +DA:56,6 +FN:56,RuleWhitelistBase.isVerified +FNDA:6,RuleWhitelistBase.isVerified +DA:63,6 +DA:69,35 +FN:69,RuleWhitelistBase.supportsInterface +FNDA:35,RuleWhitelistBase.supportsInterface +DA:72,35 +DA:73,33 +DA:80,3 +FN:80,RuleWhitelistBase.onlyCheckSpenderManager FNDA:3,RuleWhitelistBase.onlyCheckSpenderManager -DA:56,3 -DA:60,0 -FN:60,RuleWhitelistBase._authorizeCheckSpenderManager -FNDA:0,RuleWhitelistBase._authorizeCheckSpenderManager -DA:66,53 -FN:66,RuleWhitelistBase._detectTransferRestriction -FNDA:53,RuleWhitelistBase._detectTransferRestriction -DA:77,53 -BRDA:77,1,0,13 -BRDA:77,1,1,30 -DA:78,13 -DA:79,40 -BRDA:79,2,0,10 -DA:80,10 -DA:82,30 -DA:85,18 -FN:85,RuleWhitelistBase._detectTransferRestrictionFrom -FNDA:18,RuleWhitelistBase._detectTransferRestrictionFrom -DA:92,18 -BRDA:92,3,0,8 -DA:93,8 -DA:95,10 -DA:98,2 -FN:98,RuleWhitelistBase._setCheckSpender +DA:81,3 +DA:93,2 +FN:93,RuleWhitelistBase._setCheckSpender FNDA:2,RuleWhitelistBase._setCheckSpender -DA:99,2 +DA:94,2 +DA:101,0 +FN:101,RuleWhitelistBase._authorizeCheckSpenderManager +FNDA:0,RuleWhitelistBase._authorizeCheckSpenderManager +DA:109,100 +FN:109,RuleWhitelistBase._detectTransferRestriction +FNDA:100,RuleWhitelistBase._detectTransferRestriction +DA:120,100 +DA:121,100 +DA:124,100 +DA:125,100 +BRDA:125,0,0,7 +DA:126,7 +DA:131,93 +BRDA:131,1,0,30 +DA:132,30 +DA:134,63 +BRDA:134,2,0,11 +DA:135,11 +DA:137,52 +DA:148,38 +FN:148,RuleWhitelistBase._detectTransferRestrictionFrom +FNDA:38,RuleWhitelistBase._detectTransferRestrictionFrom +DA:157,38 +BRDA:157,3,0,8 +DA:158,8 +DA:160,30 FNF:9 FNH:8 -LF:27 -LH:26 -BRF:5 -BRH:5 +LF:31 +LH:30 +BRF:4 +BRH:4 end_of_record TN: SF:src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol -DA:31,44 -FN:31,RuleWhitelistWrapperBase.constructor -FNDA:44,RuleWhitelistWrapperBase.constructor -DA:32,44 -DA:39,4 -FN:39,RuleWhitelistWrapperBase.onlyCheckSpenderManager +DA:37,57 +FN:37,RuleWhitelistWrapperBase.constructor +FNDA:57,RuleWhitelistWrapperBase.constructor +DA:40,57 +DA:41,57 +DA:48,4 +FN:48,RuleWhitelistWrapperBase.onlyCheckSpenderManager FNDA:4,RuleWhitelistWrapperBase.onlyCheckSpenderManager -DA:40,4 -DA:44,0 -FN:44,RuleWhitelistWrapperBase._authorizeCheckSpenderManager -FNDA:0,RuleWhitelistWrapperBase._authorizeCheckSpenderManager -DA:58,4 -FN:58,RuleWhitelistWrapperBase.setCheckSpender +DA:49,4 +DA:65,4 +FN:65,RuleWhitelistWrapperBase.setCheckSpender FNDA:4,RuleWhitelistWrapperBase.setCheckSpender -DA:59,3 -DA:60,3 -DA:63,46 -FN:63,RuleWhitelistWrapperBase.supportsInterface -FNDA:46,RuleWhitelistWrapperBase.supportsInterface -DA:70,46 -DA:77,4 -FN:77,RuleWhitelistWrapperBase.isVerified -FNDA:4,RuleWhitelistWrapperBase.isVerified -DA:78,4 -DA:79,4 -DA:80,4 -DA:81,4 -DA:95,34 -FN:95,RuleWhitelistWrapperBase._detectTransferRestriction -FNDA:34,RuleWhitelistWrapperBase._detectTransferRestriction -DA:106,34 -DA:107,34 -DA:108,34 -DA:110,34 -DA:111,34 -BRDA:111,0,0,13 -BRDA:111,0,1,13 -DA:112,13 -DA:113,21 -BRDA:113,1,0,8 -BRDA:113,1,1,13 -DA:114,8 -DA:116,13 -DA:120,20 -FN:120,RuleWhitelistWrapperBase._detectTransferRestrictionFrom -FNDA:20,RuleWhitelistWrapperBase._detectTransferRestrictionFrom -DA:127,20 -BRDA:127,2,0,1 -DA:128,1 -DA:131,19 -DA:132,19 -DA:133,19 -DA:134,19 -DA:136,19 -DA:138,19 -BRDA:138,3,0,1 -BRDA:138,3,1,9 -DA:139,1 -DA:140,18 -BRDA:140,4,0,1 -BRDA:140,4,1,9 -DA:141,1 -DA:142,17 -BRDA:142,5,0,8 -BRDA:142,5,1,9 -DA:143,8 -DA:145,9 -DA:151,13 -FN:151,RuleWhitelistWrapperBase._transferred.0 -FNDA:13,RuleWhitelistWrapperBase._transferred.0 -DA:157,13 -DA:160,1 -FN:160,RuleWhitelistWrapperBase._transferred.1 -FNDA:1,RuleWhitelistWrapperBase._transferred.1 -DA:166,1 -DA:174,57 -FN:174,RuleWhitelistWrapperBase._detectTransferRestrictionForTargets -FNDA:57,RuleWhitelistWrapperBase._detectTransferRestrictionForTargets -DA:180,57 -DA:181,57 -DA:182,57 -DA:185,105 -DA:186,105 -DA:187,85 -BRDA:187,6,0,85 -DA:188,85 -DA:193,105 -DA:194,105 -DA:195,201 -BRDA:195,7,0,81 -DA:196,81 -DA:197,81 -DA:200,24 -BRDA:200,8,0,24 -DA:201,24 -DA:204,57 -DA:211,3 -FN:211,RuleWhitelistWrapperBase._setCheckSpender +DA:66,3 +DA:67,3 +DA:73,49 +FN:73,RuleWhitelistWrapperBase.supportsInterface +FNDA:49,RuleWhitelistWrapperBase.supportsInterface +DA:74,49 +DA:83,7 +FN:83,RuleWhitelistWrapperBase.isVerified +FNDA:7,RuleWhitelistWrapperBase.isVerified +DA:84,7 +DA:85,7 +DA:86,7 +DA:87,7 +DA:100,0 +FN:100,RuleWhitelistWrapperBase._authorizeCheckSpenderManager +FNDA:0,RuleWhitelistWrapperBase._authorizeCheckSpenderManager +DA:106,3 +FN:106,RuleWhitelistWrapperBase._setCheckSpender FNDA:3,RuleWhitelistWrapperBase._setCheckSpender -DA:212,3 -DA:222,145 -FN:222,RuleWhitelistWrapperBase._msgSender -FNDA:145,RuleWhitelistWrapperBase._msgSender -DA:223,145 -DA:229,2 -FN:229,RuleWhitelistWrapperBase._msgData +DA:107,3 +DA:117,66 +FN:117,RuleWhitelistWrapperBase._detectTransferRestriction +FNDA:66,RuleWhitelistWrapperBase._detectTransferRestriction +DA:129,66 +DA:130,66 +BRDA:130,0,0,4 +DA:131,4 +DA:134,62 +DA:135,62 +DA:142,62 +BRDA:142,1,0,2 +DA:143,2 +DA:145,3 +BRDA:145,2,0,3 +DA:146,3 +BRDA:146,3,0,1 +DA:147,1 +DA:149,2 +DA:151,2 +BRDA:151,4,0,2 +DA:152,2 +BRDA:152,5,0,1 +DA:153,1 +DA:155,1 +DA:158,55 +DA:159,55 +DA:160,55 +DA:162,55 +DA:163,54 +BRDA:163,6,0,22 +BRDA:163,6,1,24 +DA:164,22 +DA:165,32 +BRDA:165,7,0,8 +BRDA:165,7,1,24 +DA:166,8 +DA:168,24 +DA:177,5 +FN:177,RuleWhitelistWrapperBase._isListedInAnyChild +FNDA:5,RuleWhitelistWrapperBase._isListedInAnyChild +DA:178,5 +DA:179,5 +DA:180,5 +DA:191,37 +FN:191,RuleWhitelistWrapperBase._detectTransferRestrictionFrom +FNDA:37,RuleWhitelistWrapperBase._detectTransferRestrictionFrom +DA:200,37 +BRDA:200,8,0,2 +DA:201,2 +DA:204,35 +DA:205,35 +DA:206,35 +DA:207,35 +DA:209,35 +DA:211,35 +BRDA:211,9,0,9 +BRDA:211,9,1,17 +DA:212,9 +DA:213,26 +BRDA:213,10,0,1 +BRDA:213,10,1,17 +DA:214,1 +DA:215,25 +BRDA:215,11,0,8 +BRDA:215,11,1,17 +DA:216,8 +DA:218,17 +DA:230,20 +FN:230,RuleWhitelistWrapperBase._transferred.0 +FNDA:20,RuleWhitelistWrapperBase._transferred.0 +DA:236,20 +DA:246,1 +FN:246,RuleWhitelistWrapperBase._transferred.1 +FNDA:1,RuleWhitelistWrapperBase._transferred.1 +DA:252,1 +DA:260,102 +FN:260,RuleWhitelistWrapperBase._detectTransferRestrictionForTargets +FNDA:102,RuleWhitelistWrapperBase._detectTransferRestrictionForTargets +DA:266,102 +DA:267,102 +DA:268,102 +DA:271,153 +DA:272,152 +DA:273,160 +BRDA:273,12,0,160 +DA:274,160 +DA:279,152 +DA:280,152 +DA:281,278 +BRDA:281,13,0,105 +DA:282,105 +DA:283,105 +DA:286,47 +BRDA:286,14,0,47 +DA:287,47 +DA:290,101 +DA:301,175 +FN:301,RuleWhitelistWrapperBase._msgSender +FNDA:175,RuleWhitelistWrapperBase._msgSender +DA:302,175 +DA:309,2 +FN:309,RuleWhitelistWrapperBase._msgData FNDA:2,RuleWhitelistWrapperBase._msgData -DA:230,2 -DA:236,147 -FN:236,RuleWhitelistWrapperBase._contextSuffixLength -FNDA:147,RuleWhitelistWrapperBase._contextSuffixLength -DA:237,147 -FNF:15 -FNH:14 -LF:68 -LH:67 -BRF:14 -BRH:14 +DA:310,2 +DA:317,177 +FN:317,RuleWhitelistWrapperBase._contextSuffixLength +FNDA:177,RuleWhitelistWrapperBase._contextSuffixLength +DA:318,177 +FNF:16 +FNH:15 +LF:88 +LH:87 +BRF:20 +BRH:20 end_of_record TN: SF:src/rules/validation/abstract/core/RuleNFTAdapter.sol -DA:34,17 -FN:34,RuleNFTAdapter.detectTransferRestriction -FNDA:17,RuleNFTAdapter.detectTransferRestriction -DA:47,17 -DA:53,13 -FN:53,RuleNFTAdapter.detectTransferRestrictionFrom -FNDA:13,RuleNFTAdapter.detectTransferRestrictionFrom -DA:61,13 -DA:67,15 -FN:67,RuleNFTAdapter.canTransfer -FNDA:15,RuleNFTAdapter.canTransfer -DA:79,15 -DA:85,11 -FN:85,RuleNFTAdapter.canTransferFrom -FNDA:11,RuleNFTAdapter.canTransferFrom -DA:99,11 -DA:106,14 -FN:106,RuleNFTAdapter.transferred.0 -FNDA:14,RuleNFTAdapter.transferred.0 -DA:117,14 -DA:123,9 -FN:123,RuleNFTAdapter.transferred.1 -FNDA:9,RuleNFTAdapter.transferred.1 -DA:135,9 -DA:141,6 -FN:141,RuleNFTAdapter.transferred.2 -FNDA:6,RuleNFTAdapter.transferred.2 -DA:142,6 -BRDA:142,0,0,3 -BRDA:142,0,1,3 -DA:143,3 -DA:145,3 -DA:152,6 -FN:152,RuleNFTAdapter.transferred.3 -FNDA:6,RuleNFTAdapter.transferred.3 -DA:153,6 -BRDA:153,1,0,3 -BRDA:153,1,1,3 -DA:154,3 -DA:156,3 -DA:167,0 -FN:167,RuleNFTAdapter._transferred +DA:46,34 +FN:46,RuleNFTAdapter.transferred.0 +FNDA:34,RuleNFTAdapter.transferred.0 +DA:47,34 +BRDA:47,0,0,17 +BRDA:47,0,1,17 +DA:48,17 +DA:50,17 +DA:57,36 +FN:57,RuleNFTAdapter.transferred.1 +FNDA:36,RuleNFTAdapter.transferred.1 +DA:58,36 +BRDA:58,1,0,17 +BRDA:58,1,1,19 +DA:59,17 +DA:61,19 +DA:72,28 +FN:72,RuleNFTAdapter.transferred.2 +FNDA:28,RuleNFTAdapter.transferred.2 +DA:83,28 +DA:89,23 +FN:89,RuleNFTAdapter.transferred.3 +FNDA:23,RuleNFTAdapter.transferred.3 +DA:101,23 +DA:107,31 +FN:107,RuleNFTAdapter.detectTransferRestriction +FNDA:31,RuleNFTAdapter.detectTransferRestriction +DA:120,31 +DA:126,27 +FN:126,RuleNFTAdapter.detectTransferRestrictionFrom +FNDA:27,RuleNFTAdapter.detectTransferRestrictionFrom +DA:140,27 +DA:146,29 +FN:146,RuleNFTAdapter.canTransfer +FNDA:29,RuleNFTAdapter.canTransfer +DA:158,29 +DA:164,25 +FN:164,RuleNFTAdapter.canTransferFrom +FNDA:25,RuleNFTAdapter.canTransferFrom +DA:178,25 +DA:192,0 +FN:192,RuleNFTAdapter._transferred FNDA:0,RuleNFTAdapter._transferred -DA:172,0 -FN:172,RuleNFTAdapter._transferredFrom +DA:201,0 +FN:201,RuleNFTAdapter._transferredFrom FNDA:0,RuleNFTAdapter._transferredFrom FNF:10 FNH:8 @@ -1313,121 +1861,163 @@ BRH:4 end_of_record TN: SF:src/rules/validation/abstract/core/RuleTransferValidation.sol -DA:32,351 -FN:32,RuleTransferValidation.detectTransferRestriction -FNDA:351,RuleTransferValidation.detectTransferRestriction -DA:39,351 -DA:45,38 -FN:45,RuleTransferValidation.detectTransferRestrictionFrom -FNDA:38,RuleTransferValidation.detectTransferRestrictionFrom -DA:52,38 -DA:63,21 -FN:63,RuleTransferValidation.canTransfer -FNDA:21,RuleTransferValidation.canTransfer -DA:69,21 -DA:75,14 -FN:75,RuleTransferValidation.canTransferFrom -FNDA:14,RuleTransferValidation.canTransferFrom -DA:82,14 -DA:86,198 -FN:86,RuleTransferValidation.supportsInterface -FNDA:198,RuleTransferValidation.supportsInterface -DA:87,198 -DA:88,197 -DA:89,196 -DA:90,108 -DA:91,108 -DA:105,0 -FN:105,RuleTransferValidation._detectTransferRestriction +DA:36,923 +FN:36,RuleTransferValidation.detectTransferRestriction +FNDA:923,RuleTransferValidation.detectTransferRestriction +DA:43,923 +DA:49,58 +FN:49,RuleTransferValidation.detectTransferRestrictionFrom +FNDA:58,RuleTransferValidation.detectTransferRestrictionFrom +DA:56,58 +DA:67,37 +FN:67,RuleTransferValidation.canTransfer +FNDA:37,RuleTransferValidation.canTransfer +DA:73,37 +DA:79,30 +FN:79,RuleTransferValidation.canTransferFrom +FNDA:30,RuleTransferValidation.canTransferFrom +DA:86,30 +DA:95,230 +FN:95,RuleTransferValidation.supportsInterface +FNDA:230,RuleTransferValidation.supportsInterface +DA:96,230 +DA:97,229 +DA:98,228 +DA:99,133 +DA:113,0 +FN:113,RuleTransferValidation._detectTransferRestriction FNDA:0,RuleTransferValidation._detectTransferRestriction -DA:119,0 -FN:119,RuleTransferValidation._detectTransferRestrictionFrom +DA:127,0 +FN:127,RuleTransferValidation._detectTransferRestrictionFrom FNDA:0,RuleTransferValidation._detectTransferRestrictionFrom FNF:7 FNH:5 -LF:16 -LH:14 +LF:15 +LH:13 BRF:0 BRH:0 end_of_record TN: SF:src/rules/validation/abstract/core/RuleWhitelistShared.sol -DA:36,10 -FN:36,RuleWhitelistShared.canReturnTransferRestrictionCode +DA:46,13 +FN:46,RuleWhitelistShared.onlyMintBurnManager +FNDA:13,RuleWhitelistShared.onlyMintBurnManager +DA:47,13 +DA:62,10 +FN:62,RuleWhitelistShared.canReturnTransferRestrictionCode FNDA:10,RuleWhitelistShared.canReturnTransferRestrictionCode -DA:37,10 -DA:38,5 -DA:39,2 -DA:49,14 -FN:49,RuleWhitelistShared.messageForTransferRestriction -FNDA:14,RuleWhitelistShared.messageForTransferRestriction -DA:55,14 -BRDA:55,0,0,6 -BRDA:55,0,1,2 -DA:56,6 -DA:57,8 -BRDA:57,1,0,4 -BRDA:57,1,1,2 -DA:58,4 -DA:59,4 -BRDA:59,2,0,2 -BRDA:59,2,1,2 -DA:60,2 -DA:62,2 -DA:81,19 -FN:81,RuleWhitelistShared.transferred.0 +DA:63,10 +DA:64,5 +DA:65,2 +DA:66,2 +DA:73,13 +FN:73,RuleWhitelistShared.setAllowMint +FNDA:13,RuleWhitelistShared.setAllowMint +DA:74,10 +DA:75,10 +DA:82,7 +FN:82,RuleWhitelistShared.setAllowBurn +FNDA:7,RuleWhitelistShared.setAllowBurn +DA:83,5 +DA:84,5 +DA:94,19 +FN:94,RuleWhitelistShared.messageForTransferRestriction +FNDA:19,RuleWhitelistShared.messageForTransferRestriction +DA:100,19 +BRDA:100,0,0,6 +BRDA:100,0,1,2 +DA:101,6 +DA:102,13 +BRDA:102,1,0,4 +BRDA:102,1,1,2 +DA:103,4 +DA:104,9 +BRDA:104,2,0,2 +BRDA:104,2,1,2 +DA:105,2 +DA:106,7 +BRDA:106,3,0,3 +BRDA:106,3,1,2 +DA:107,3 +DA:108,4 +BRDA:108,4,0,2 +BRDA:108,4,1,2 +DA:109,2 +DA:111,2 +DA:130,19 +FN:130,RuleWhitelistShared.transferred.0 FNDA:19,RuleWhitelistShared.transferred.0 -DA:82,19 -DA:96,4 -FN:96,RuleWhitelistShared.transferred.1 -FNDA:4,RuleWhitelistShared.transferred.1 -DA:97,4 -DA:104,28 -FN:104,RuleWhitelistShared._transferred -FNDA:28,RuleWhitelistShared._transferred -DA:105,28 -DA:106,28 -BRDA:106,3,0,14 -BRDA:106,3,1,14 -DA:112,11 -FN:112,RuleWhitelistShared._transferredFrom -FNDA:11,RuleWhitelistShared._transferredFrom -DA:113,11 -DA:114,11 -BRDA:114,4,0,4 -BRDA:114,4,1,7 -FNF:6 -FNH:6 -LF:22 -LH:22 -BRF:10 -BRH:10 +DA:131,19 +DA:145,13 +FN:145,RuleWhitelistShared.transferred.1 +FNDA:13,RuleWhitelistShared.transferred.1 +DA:146,13 +DA:158,245 +FN:158,RuleWhitelistShared._setAllowMintBurn +FNDA:245,RuleWhitelistShared._setAllowMintBurn +DA:159,245 +DA:160,245 +DA:161,245 +DA:162,245 +DA:172,166 +FN:172,RuleWhitelistShared._detectMintBurnRestriction +FNDA:166,RuleWhitelistShared._detectMintBurnRestriction +DA:173,166 +BRDA:173,5,0,8 +DA:174,8 +DA:176,158 +BRDA:176,6,0,3 +DA:177,3 +DA:179,155 +DA:185,0 +FN:185,RuleWhitelistShared._authorizeMintBurnManager +FNDA:0,RuleWhitelistShared._authorizeMintBurnManager +DA:190,40 +FN:190,RuleWhitelistShared._transferred +FNDA:40,RuleWhitelistShared._transferred +DA:191,40 +DA:192,40 +BRDA:192,7,0,22 +BRDA:192,7,1,18 +DA:201,32 +FN:201,RuleWhitelistShared._transferredFrom +FNDA:32,RuleWhitelistShared._transferredFrom +DA:202,32 +DA:203,32 +BRDA:203,8,0,12 +BRDA:203,8,1,20 +FNF:12 +FNH:11 +LF:47 +LH:46 +BRF:16 +BRH:16 end_of_record TN: SF:src/rules/validation/deployment/RuleBlacklist.sol -DA:32,91 -FN:32,RuleBlacklist.supportsInterface -FNDA:91,RuleBlacklist.supportsInterface -DA:39,91 -DA:40,61 -DA:47,28 -FN:47,RuleBlacklist._authorizeAddressListAdd -FNDA:28,RuleBlacklist._authorizeAddressListAdd -DA:49,1 -FN:49,RuleBlacklist._authorizeAddressListRemove +DA:37,92 +FN:37,RuleBlacklist.supportsInterface +FNDA:92,RuleBlacklist.supportsInterface +DA:44,92 +DA:45,62 +DA:55,30 +FN:55,RuleBlacklist._authorizeAddressListAdd +FNDA:30,RuleBlacklist._authorizeAddressListAdd +DA:60,1 +FN:60,RuleBlacklist._authorizeAddressListRemove FNDA:1,RuleBlacklist._authorizeAddressListRemove -DA:55,83 -FN:55,RuleBlacklist._msgSender -FNDA:83,RuleBlacklist._msgSender -DA:56,83 -DA:59,1 -FN:59,RuleBlacklist._msgData +DA:70,88 +FN:70,RuleBlacklist._msgSender +FNDA:88,RuleBlacklist._msgSender +DA:71,88 +DA:78,1 +FN:78,RuleBlacklist._msgData FNDA:1,RuleBlacklist._msgData -DA:60,1 -DA:63,84 -FN:63,RuleBlacklist._contextSuffixLength -FNDA:84,RuleBlacklist._contextSuffixLength -DA:64,84 +DA:79,1 +DA:86,89 +FN:86,RuleBlacklist._contextSuffixLength +FNDA:89,RuleBlacklist._contextSuffixLength +DA:87,89 FNF:6 FNH:6 LF:11 @@ -1437,111 +2027,126 @@ BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol -DA:25,2 -FN:25,RuleBlacklistOwnable2Step._authorizeAddressListAdd +DA:36,6 +FN:36,RuleBlacklistOwnable2Step.supportsInterface +FNDA:6,RuleBlacklistOwnable2Step.supportsInterface +DA:43,6 +DA:44,3 +DA:54,2 +FN:54,RuleBlacklistOwnable2Step._authorizeAddressListAdd FNDA:2,RuleBlacklistOwnable2Step._authorizeAddressListAdd -DA:27,2 -FN:27,RuleBlacklistOwnable2Step._authorizeAddressListRemove +DA:59,2 +FN:59,RuleBlacklistOwnable2Step._authorizeAddressListRemove FNDA:2,RuleBlacklistOwnable2Step._authorizeAddressListRemove -DA:33,12 -FN:33,RuleBlacklistOwnable2Step._msgSender +DA:69,12 +FN:69,RuleBlacklistOwnable2Step._msgSender FNDA:12,RuleBlacklistOwnable2Step._msgSender -DA:34,12 -DA:37,1 -FN:37,RuleBlacklistOwnable2Step._msgData +DA:70,12 +DA:77,1 +FN:77,RuleBlacklistOwnable2Step._msgData FNDA:1,RuleBlacklistOwnable2Step._msgData -DA:38,1 -DA:41,13 -FN:41,RuleBlacklistOwnable2Step._contextSuffixLength +DA:78,1 +DA:85,13 +FN:85,RuleBlacklistOwnable2Step._contextSuffixLength FNDA:13,RuleBlacklistOwnable2Step._contextSuffixLength -DA:42,13 -FNF:5 -FNH:5 -LF:8 -LH:8 +DA:86,13 +FNF:6 +FNH:6 +LF:11 +LH:11 BRF:0 BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleERC2980.sol -DA:51,1 -FN:51,RuleERC2980.supportsInterface +DA:56,1 +FN:56,RuleERC2980.supportsInterface FNDA:1,RuleERC2980.supportsInterface -DA:58,1 -DA:65,44 -FN:65,RuleERC2980._authorizeWhitelistAdd -FNDA:44,RuleERC2980._authorizeWhitelistAdd -DA:67,8 -FN:67,RuleERC2980._authorizeWhitelistRemove +DA:63,1 +DA:76,5 +FN:76,RuleERC2980._authorizeMintBurnManager +FNDA:5,RuleERC2980._authorizeMintBurnManager +DA:78,48 +FN:78,RuleERC2980._authorizeWhitelistAdd +FNDA:48,RuleERC2980._authorizeWhitelistAdd +DA:83,8 +FN:83,RuleERC2980._authorizeWhitelistRemove FNDA:8,RuleERC2980._authorizeWhitelistRemove -DA:69,22 -FN:69,RuleERC2980._authorizeFrozenlistAdd -FNDA:22,RuleERC2980._authorizeFrozenlistAdd -DA:71,7 -FN:71,RuleERC2980._authorizeFrozenlistRemove +DA:88,24 +FN:88,RuleERC2980._authorizeFrozenlistAdd +FNDA:24,RuleERC2980._authorizeFrozenlistAdd +DA:93,7 +FN:93,RuleERC2980._authorizeFrozenlistRemove FNDA:7,RuleERC2980._authorizeFrozenlistRemove -DA:77,242 -FN:77,RuleERC2980._msgSender -FNDA:242,RuleERC2980._msgSender -DA:78,242 -DA:81,1 -FN:81,RuleERC2980._msgData +DA:103,259 +FN:103,RuleERC2980._msgSender +FNDA:259,RuleERC2980._msgSender +DA:104,259 +DA:111,1 +FN:111,RuleERC2980._msgData FNDA:1,RuleERC2980._msgData -DA:82,1 -DA:85,243 -FN:85,RuleERC2980._contextSuffixLength -FNDA:243,RuleERC2980._contextSuffixLength -DA:86,243 -FNF:8 -FNH:8 -LF:12 -LH:12 +DA:112,1 +DA:119,260 +FN:119,RuleERC2980._contextSuffixLength +FNDA:260,RuleERC2980._contextSuffixLength +DA:120,260 +FNF:9 +FNH:9 +LF:13 +LH:13 BRF:0 BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleERC2980Ownable2Step.sol -DA:33,7 -FN:33,RuleERC2980Ownable2Step._authorizeWhitelistAdd +DA:39,5 +FN:39,RuleERC2980Ownable2Step.supportsInterface +FNDA:5,RuleERC2980Ownable2Step.supportsInterface +DA:46,5 +DA:59,3 +FN:59,RuleERC2980Ownable2Step._authorizeMintBurnManager +FNDA:3,RuleERC2980Ownable2Step._authorizeMintBurnManager +DA:61,7 +FN:61,RuleERC2980Ownable2Step._authorizeWhitelistAdd FNDA:7,RuleERC2980Ownable2Step._authorizeWhitelistAdd -DA:35,3 -FN:35,RuleERC2980Ownable2Step._authorizeWhitelistRemove +DA:66,3 +FN:66,RuleERC2980Ownable2Step._authorizeWhitelistRemove FNDA:3,RuleERC2980Ownable2Step._authorizeWhitelistRemove -DA:37,6 -FN:37,RuleERC2980Ownable2Step._authorizeFrozenlistAdd +DA:71,6 +FN:71,RuleERC2980Ownable2Step._authorizeFrozenlistAdd FNDA:6,RuleERC2980Ownable2Step._authorizeFrozenlistAdd -DA:39,2 -FN:39,RuleERC2980Ownable2Step._authorizeFrozenlistRemove +DA:76,2 +FN:76,RuleERC2980Ownable2Step._authorizeFrozenlistRemove FNDA:2,RuleERC2980Ownable2Step._authorizeFrozenlistRemove -DA:45,30 -FN:45,RuleERC2980Ownable2Step._msgSender -FNDA:30,RuleERC2980Ownable2Step._msgSender -DA:46,30 -DA:49,1 -FN:49,RuleERC2980Ownable2Step._msgData +DA:86,34 +FN:86,RuleERC2980Ownable2Step._msgSender +FNDA:34,RuleERC2980Ownable2Step._msgSender +DA:87,34 +DA:94,1 +FN:94,RuleERC2980Ownable2Step._msgData FNDA:1,RuleERC2980Ownable2Step._msgData -DA:50,1 -DA:53,31 -FN:53,RuleERC2980Ownable2Step._contextSuffixLength -FNDA:31,RuleERC2980Ownable2Step._contextSuffixLength -DA:54,31 -FNF:7 -FNH:7 -LF:10 -LH:10 +DA:95,1 +DA:102,35 +FN:102,RuleERC2980Ownable2Step._contextSuffixLength +FNDA:35,RuleERC2980Ownable2Step._contextSuffixLength +DA:103,35 +FNF:9 +FNH:9 +LF:13 +LH:13 BRF:0 BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleIdentityRegistry.sol -DA:28,15 -FN:28,RuleIdentityRegistry.supportsInterface -FNDA:15,RuleIdentityRegistry.supportsInterface -DA:35,15 -DA:36,10 -DA:43,5 -FN:43,RuleIdentityRegistry._authorizeIdentityRegistryManager -FNDA:5,RuleIdentityRegistry._authorizeIdentityRegistryManager +DA:38,27 +FN:38,RuleIdentityRegistry.supportsInterface +FNDA:27,RuleIdentityRegistry.supportsInterface +DA:45,27 +DA:46,18 +DA:56,12 +FN:56,RuleIdentityRegistry._authorizeIdentityRegistryManager +FNDA:12,RuleIdentityRegistry._authorizeIdentityRegistryManager FNF:2 FNH:2 LF:4 @@ -1551,25 +2156,30 @@ BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol -DA:23,4 -FN:23,RuleIdentityRegistryOwnable2Step._authorizeIdentityRegistryManager +DA:38,5 +FN:38,RuleIdentityRegistryOwnable2Step.supportsInterface +FNDA:5,RuleIdentityRegistryOwnable2Step.supportsInterface +DA:45,5 +DA:46,2 +DA:56,4 +FN:56,RuleIdentityRegistryOwnable2Step._authorizeIdentityRegistryManager FNDA:4,RuleIdentityRegistryOwnable2Step._authorizeIdentityRegistryManager -FNF:1 -FNH:1 -LF:1 -LH:1 +FNF:2 +FNH:2 +LF:4 +LH:4 BRF:0 BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleMaxTotalSupply.sol -DA:32,15 -FN:32,RuleMaxTotalSupply.supportsInterface -FNDA:15,RuleMaxTotalSupply.supportsInterface -DA:39,15 -DA:40,10 -DA:47,260 -FN:47,RuleMaxTotalSupply._authorizeMaxTotalSupplyManager +DA:37,19 +FN:37,RuleMaxTotalSupply.supportsInterface +FNDA:19,RuleMaxTotalSupply.supportsInterface +DA:44,19 +DA:45,13 +DA:55,260 +FN:55,RuleMaxTotalSupply._authorizeMaxTotalSupplyManager FNDA:260,RuleMaxTotalSupply._authorizeMaxTotalSupplyManager FNF:2 FNH:2 @@ -1580,38 +2190,43 @@ BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol -DA:26,4 -FN:26,RuleMaxTotalSupplyOwnable2Step._authorizeMaxTotalSupplyManager +DA:39,5 +FN:39,RuleMaxTotalSupplyOwnable2Step.supportsInterface +FNDA:5,RuleMaxTotalSupplyOwnable2Step.supportsInterface +DA:46,5 +DA:47,2 +DA:57,4 +FN:57,RuleMaxTotalSupplyOwnable2Step._authorizeMaxTotalSupplyManager FNDA:4,RuleMaxTotalSupplyOwnable2Step._authorizeMaxTotalSupplyManager -FNF:1 -FNH:1 -LF:1 -LH:1 +FNF:2 +FNH:2 +LF:4 +LH:4 BRF:0 BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleSanctionsList.sol -DA:34,58 -FN:34,RuleSanctionsList.supportsInterface +DA:40,58 +FN:40,RuleSanctionsList.supportsInterface FNDA:58,RuleSanctionsList.supportsInterface -DA:41,58 -DA:42,39 -DA:49,17 -FN:49,RuleSanctionsList._authorizeSanctionListManager -FNDA:17,RuleSanctionsList._authorizeSanctionListManager -DA:55,57 -FN:55,RuleSanctionsList._msgSender -FNDA:57,RuleSanctionsList._msgSender -DA:56,57 -DA:59,1 -FN:59,RuleSanctionsList._msgData +DA:47,58 +DA:48,39 +DA:58,18 +FN:58,RuleSanctionsList._authorizeSanctionListManager +FNDA:18,RuleSanctionsList._authorizeSanctionListManager +DA:68,60 +FN:68,RuleSanctionsList._msgSender +FNDA:60,RuleSanctionsList._msgSender +DA:69,60 +DA:76,1 +FN:76,RuleSanctionsList._msgData FNDA:1,RuleSanctionsList._msgData -DA:60,1 -DA:63,58 -FN:63,RuleSanctionsList._contextSuffixLength -FNDA:58,RuleSanctionsList._contextSuffixLength -DA:64,58 +DA:77,1 +DA:84,61 +FN:84,RuleSanctionsList._contextSuffixLength +FNDA:61,RuleSanctionsList._contextSuffixLength +DA:85,61 FNF:5 FNH:5 LF:10 @@ -1621,53 +2236,58 @@ BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol -DA:29,3 -FN:29,RuleSanctionsListOwnable2Step._authorizeSanctionListManager +DA:42,5 +FN:42,RuleSanctionsListOwnable2Step.supportsInterface +FNDA:5,RuleSanctionsListOwnable2Step.supportsInterface +DA:49,5 +DA:50,2 +DA:60,3 +FN:60,RuleSanctionsListOwnable2Step._authorizeSanctionListManager FNDA:3,RuleSanctionsListOwnable2Step._authorizeSanctionListManager -DA:35,11 -FN:35,RuleSanctionsListOwnable2Step._msgSender +DA:70,11 +FN:70,RuleSanctionsListOwnable2Step._msgSender FNDA:11,RuleSanctionsListOwnable2Step._msgSender -DA:36,11 -DA:39,1 -FN:39,RuleSanctionsListOwnable2Step._msgData +DA:71,11 +DA:78,1 +FN:78,RuleSanctionsListOwnable2Step._msgData FNDA:1,RuleSanctionsListOwnable2Step._msgData -DA:40,1 -DA:43,13 -FN:43,RuleSanctionsListOwnable2Step._contextSuffixLength +DA:79,1 +DA:86,13 +FN:86,RuleSanctionsListOwnable2Step._contextSuffixLength FNDA:13,RuleSanctionsListOwnable2Step._contextSuffixLength -DA:44,13 -FNF:4 -FNH:4 -LF:7 -LH:7 +DA:87,13 +FNF:5 +FNH:5 +LF:10 +LH:10 BRF:0 BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleSpenderWhitelist.sol -DA:29,5 -FN:29,RuleSpenderWhitelist.supportsInterface -FNDA:5,RuleSpenderWhitelist.supportsInterface -DA:36,5 -DA:37,4 -DA:44,6 -FN:44,RuleSpenderWhitelist._authorizeAddressListAdd -FNDA:6,RuleSpenderWhitelist._authorizeAddressListAdd -DA:46,2 -FN:46,RuleSpenderWhitelist._authorizeAddressListRemove +DA:38,6 +FN:38,RuleSpenderWhitelist.supportsInterface +FNDA:6,RuleSpenderWhitelist.supportsInterface +DA:45,6 +DA:46,5 +DA:56,7 +FN:56,RuleSpenderWhitelist._authorizeAddressListAdd +FNDA:7,RuleSpenderWhitelist._authorizeAddressListAdd +DA:61,2 +FN:61,RuleSpenderWhitelist._authorizeAddressListRemove FNDA:2,RuleSpenderWhitelist._authorizeAddressListRemove -DA:52,31 -FN:52,RuleSpenderWhitelist._msgSender -FNDA:31,RuleSpenderWhitelist._msgSender -DA:53,31 -DA:56,1 -FN:56,RuleSpenderWhitelist._msgData +DA:71,38 +FN:71,RuleSpenderWhitelist._msgSender +FNDA:38,RuleSpenderWhitelist._msgSender +DA:72,38 +DA:79,1 +FN:79,RuleSpenderWhitelist._msgData FNDA:1,RuleSpenderWhitelist._msgData -DA:57,1 -DA:60,33 -FN:60,RuleSpenderWhitelist._contextSuffixLength -FNDA:33,RuleSpenderWhitelist._contextSuffixLength -DA:61,33 +DA:80,1 +DA:87,40 +FN:87,RuleSpenderWhitelist._contextSuffixLength +FNDA:40,RuleSpenderWhitelist._contextSuffixLength +DA:88,40 FNF:6 FNH:6 LF:11 @@ -1677,164 +2297,197 @@ BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol -DA:28,2 -FN:28,RuleSpenderWhitelistOwnable2Step._authorizeAddressListAdd +DA:39,6 +FN:39,RuleSpenderWhitelistOwnable2Step.supportsInterface +FNDA:6,RuleSpenderWhitelistOwnable2Step.supportsInterface +DA:46,6 +DA:47,3 +DA:57,2 +FN:57,RuleSpenderWhitelistOwnable2Step._authorizeAddressListAdd FNDA:2,RuleSpenderWhitelistOwnable2Step._authorizeAddressListAdd -DA:30,2 -FN:30,RuleSpenderWhitelistOwnable2Step._authorizeAddressListRemove +DA:62,2 +FN:62,RuleSpenderWhitelistOwnable2Step._authorizeAddressListRemove FNDA:2,RuleSpenderWhitelistOwnable2Step._authorizeAddressListRemove -DA:36,10 -FN:36,RuleSpenderWhitelistOwnable2Step._msgSender +DA:72,10 +FN:72,RuleSpenderWhitelistOwnable2Step._msgSender FNDA:10,RuleSpenderWhitelistOwnable2Step._msgSender -DA:37,10 -DA:40,1 -FN:40,RuleSpenderWhitelistOwnable2Step._msgData +DA:73,10 +DA:80,1 +FN:80,RuleSpenderWhitelistOwnable2Step._msgData FNDA:1,RuleSpenderWhitelistOwnable2Step._msgData -DA:41,1 -DA:44,12 -FN:44,RuleSpenderWhitelistOwnable2Step._contextSuffixLength +DA:81,1 +DA:88,12 +FN:88,RuleSpenderWhitelistOwnable2Step._contextSuffixLength FNDA:12,RuleSpenderWhitelistOwnable2Step._contextSuffixLength -DA:45,12 -FNF:5 -FNH:5 -LF:8 -LH:8 +DA:89,12 +FNF:6 +FNH:6 +LF:11 +LH:11 BRF:0 BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleWhitelist.sol -DA:47,40 +DA:47,47 FN:47,RuleWhitelist.supportsInterface -FNDA:40,RuleWhitelist.supportsInterface -DA:54,40 -DA:55,27 -DA:62,1 -FN:62,RuleWhitelist._authorizeCheckSpenderManager +FNDA:47,RuleWhitelist.supportsInterface +DA:54,47 +DA:55,32 +DA:65,1 +FN:65,RuleWhitelist._authorizeCheckSpenderManager FNDA:1,RuleWhitelist._authorizeCheckSpenderManager -DA:64,352 -FN:64,RuleWhitelist._authorizeAddressListAdd -FNDA:352,RuleWhitelist._authorizeAddressListAdd -DA:66,263 -FN:66,RuleWhitelist._authorizeAddressListRemove +DA:70,10 +FN:70,RuleWhitelist._authorizeMintBurnManager +FNDA:10,RuleWhitelist._authorizeMintBurnManager +DA:75,367 +FN:75,RuleWhitelist._authorizeAddressListAdd +FNDA:367,RuleWhitelist._authorizeAddressListAdd +DA:80,263 +FN:80,RuleWhitelist._authorizeAddressListRemove FNDA:263,RuleWhitelist._authorizeAddressListRemove -DA:72,780 -FN:72,RuleWhitelist._msgSender -FNDA:780,RuleWhitelist._msgSender -DA:73,780 -DA:76,1 -FN:76,RuleWhitelist._msgData +DA:90,827 +FN:90,RuleWhitelist._msgSender +FNDA:827,RuleWhitelist._msgSender +DA:91,827 +DA:98,1 +FN:98,RuleWhitelist._msgData FNDA:1,RuleWhitelist._msgData -DA:77,1 -DA:80,781 -FN:80,RuleWhitelist._contextSuffixLength -FNDA:781,RuleWhitelist._contextSuffixLength -DA:81,781 -FNF:7 -FNH:7 -LF:12 -LH:12 +DA:99,1 +DA:106,828 +FN:106,RuleWhitelist._contextSuffixLength +FNDA:828,RuleWhitelist._contextSuffixLength +DA:107,828 +FNF:8 +FNH:8 +LF:13 +LH:13 BRF:0 BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol -DA:34,2 -FN:34,RuleWhitelistOwnable2Step._authorizeAddressListAdd +DA:40,6 +FN:40,RuleWhitelistOwnable2Step.supportsInterface +FNDA:6,RuleWhitelistOwnable2Step.supportsInterface +DA:47,6 +DA:48,3 +DA:58,2 +FN:58,RuleWhitelistOwnable2Step._authorizeAddressListAdd FNDA:2,RuleWhitelistOwnable2Step._authorizeAddressListAdd -DA:36,2 -FN:36,RuleWhitelistOwnable2Step._authorizeAddressListRemove +DA:63,2 +FN:63,RuleWhitelistOwnable2Step._authorizeAddressListRemove FNDA:2,RuleWhitelistOwnable2Step._authorizeAddressListRemove -DA:38,2 -FN:38,RuleWhitelistOwnable2Step._authorizeCheckSpenderManager +DA:68,2 +FN:68,RuleWhitelistOwnable2Step._authorizeCheckSpenderManager FNDA:2,RuleWhitelistOwnable2Step._authorizeCheckSpenderManager -DA:44,15 -FN:44,RuleWhitelistOwnable2Step._msgSender -FNDA:15,RuleWhitelistOwnable2Step._msgSender -DA:45,15 -DA:48,1 -FN:48,RuleWhitelistOwnable2Step._msgData +DA:73,3 +FN:73,RuleWhitelistOwnable2Step._authorizeMintBurnManager +FNDA:3,RuleWhitelistOwnable2Step._authorizeMintBurnManager +DA:83,19 +FN:83,RuleWhitelistOwnable2Step._msgSender +FNDA:19,RuleWhitelistOwnable2Step._msgSender +DA:84,19 +DA:91,1 +FN:91,RuleWhitelistOwnable2Step._msgData FNDA:1,RuleWhitelistOwnable2Step._msgData -DA:49,1 -DA:52,16 -FN:52,RuleWhitelistOwnable2Step._contextSuffixLength -FNDA:16,RuleWhitelistOwnable2Step._contextSuffixLength -DA:53,16 -FNF:6 -FNH:6 -LF:9 -LH:9 +DA:92,1 +DA:99,20 +FN:99,RuleWhitelistOwnable2Step._contextSuffixLength +FNDA:20,RuleWhitelistOwnable2Step._contextSuffixLength +DA:100,20 +FNF:8 +FNH:8 +LF:13 +LH:13 BRF:0 BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleWhitelistWrapper.sol -DA:35,38 -FN:35,RuleWhitelistWrapper.hasRole -FNDA:38,RuleWhitelistWrapper.hasRole -DA:42,134 -DA:45,46 -FN:45,RuleWhitelistWrapper.supportsInterface -FNDA:46,RuleWhitelistWrapper.supportsInterface -DA:52,46 -DA:53,31 -DA:60,2 -FN:60,RuleWhitelistWrapper._authorizeCheckSpenderManager +DA:47,49 +FN:47,RuleWhitelistWrapper.hasRole +FNDA:49,RuleWhitelistWrapper.hasRole +DA:48,159 +DA:56,47 +FN:56,RuleWhitelistWrapper.supportsInterface +FNDA:47,RuleWhitelistWrapper.supportsInterface +DA:63,47 +DA:64,32 +DA:74,2 +FN:74,RuleWhitelistWrapper._authorizeCheckSpenderManager FNDA:2,RuleWhitelistWrapper._authorizeCheckSpenderManager -DA:65,90 -FN:65,RuleWhitelistWrapper._onlyRulesManager -FNDA:90,RuleWhitelistWrapper._onlyRulesManager -DA:71,133 -FN:71,RuleWhitelistWrapper._msgSender -FNDA:133,RuleWhitelistWrapper._msgSender -DA:72,133 -DA:75,1 -FN:75,RuleWhitelistWrapper._msgData -FNDA:1,RuleWhitelistWrapper._msgData -DA:76,1 -DA:79,134 -FN:79,RuleWhitelistWrapper._contextSuffixLength -FNDA:134,RuleWhitelistWrapper._contextSuffixLength -DA:86,134 -DA:89,38 -FN:89,RuleWhitelistWrapper._grantRole -FNDA:38,RuleWhitelistWrapper._grantRole -DA:95,38 -DA:98,1 -FN:98,RuleWhitelistWrapper._revokeRole +DA:79,4 +FN:79,RuleWhitelistWrapper._authorizeMintBurnManager +FNDA:4,RuleWhitelistWrapper._authorizeMintBurnManager +DA:85,98 +FN:85,RuleWhitelistWrapper._onlyRulesManager +FNDA:98,RuleWhitelistWrapper._onlyRulesManager +DA:90,2 +FN:90,RuleWhitelistWrapper._onlyRulesLimitManager +FNDA:2,RuleWhitelistWrapper._onlyRulesLimitManager +DA:102,49 +FN:102,RuleWhitelistWrapper._grantRole +FNDA:49,RuleWhitelistWrapper._grantRole +DA:103,49 +DA:112,1 +FN:112,RuleWhitelistWrapper._revokeRole FNDA:1,RuleWhitelistWrapper._revokeRole -DA:104,1 -FNF:9 -FNH:9 -LF:17 -LH:17 +DA:113,1 +DA:120,158 +FN:120,RuleWhitelistWrapper._msgSender +FNDA:158,RuleWhitelistWrapper._msgSender +DA:121,158 +DA:128,1 +FN:128,RuleWhitelistWrapper._msgData +FNDA:1,RuleWhitelistWrapper._msgData +DA:129,1 +DA:136,159 +FN:136,RuleWhitelistWrapper._contextSuffixLength +FNDA:159,RuleWhitelistWrapper._contextSuffixLength +DA:143,159 +FNF:11 +FNH:11 +LF:19 +LH:19 BRF:0 BRH:0 end_of_record TN: SF:src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol -DA:32,2 -FN:32,RuleWhitelistWrapperOwnable2Step._authorizeCheckSpenderManager +DA:40,5 +FN:40,RuleWhitelistWrapperOwnable2Step.supportsInterface +FNDA:5,RuleWhitelistWrapperOwnable2Step.supportsInterface +DA:47,5 +DA:48,2 +DA:58,2 +FN:58,RuleWhitelistWrapperOwnable2Step._authorizeCheckSpenderManager FNDA:2,RuleWhitelistWrapperOwnable2Step._authorizeCheckSpenderManager -DA:37,2 -FN:37,RuleWhitelistWrapperOwnable2Step._onlyRulesManager +DA:63,3 +FN:63,RuleWhitelistWrapperOwnable2Step._authorizeMintBurnManager +FNDA:3,RuleWhitelistWrapperOwnable2Step._authorizeMintBurnManager +DA:69,2 +FN:69,RuleWhitelistWrapperOwnable2Step._onlyRulesManager FNDA:2,RuleWhitelistWrapperOwnable2Step._onlyRulesManager -DA:43,12 -FN:43,RuleWhitelistWrapperOwnable2Step._msgSender -FNDA:12,RuleWhitelistWrapperOwnable2Step._msgSender -DA:44,12 -DA:47,1 -FN:47,RuleWhitelistWrapperOwnable2Step._msgData +DA:74,1 +FN:74,RuleWhitelistWrapperOwnable2Step._onlyRulesLimitManager +FNDA:1,RuleWhitelistWrapperOwnable2Step._onlyRulesLimitManager +DA:84,17 +FN:84,RuleWhitelistWrapperOwnable2Step._msgSender +FNDA:17,RuleWhitelistWrapperOwnable2Step._msgSender +DA:85,17 +DA:92,1 +FN:92,RuleWhitelistWrapperOwnable2Step._msgData FNDA:1,RuleWhitelistWrapperOwnable2Step._msgData -DA:48,1 -DA:51,13 -FN:51,RuleWhitelistWrapperOwnable2Step._contextSuffixLength -FNDA:13,RuleWhitelistWrapperOwnable2Step._contextSuffixLength -DA:58,13 -FNF:5 -FNH:5 -LF:8 -LH:8 +DA:93,1 +DA:100,18 +FN:100,RuleWhitelistWrapperOwnable2Step._contextSuffixLength +FNDA:18,RuleWhitelistWrapperOwnable2Step._contextSuffixLength +DA:107,18 +FNF:8 +FNH:8 +LF:13 +LH:13 BRF:0 BRH:0 end_of_record diff --git a/doc/img/readme-erc3643-integration.png b/doc/img/readme-erc3643-integration.png new file mode 100644 index 0000000..983005b Binary files /dev/null and b/doc/img/readme-erc3643-integration.png differ diff --git a/doc/img/readme-erc3643-integration.puml b/doc/img/readme-erc3643-integration.puml new file mode 100644 index 0000000..d4ba3cf --- /dev/null +++ b/doc/img/readme-erc3643-integration.puml @@ -0,0 +1,32 @@ +@startuml +title Using a rule with an ERC-3643 token (through a RuleEngine) + +participant "ERC-3643 token" as token +participant "RuleEngine\n(full ICompliance)" as engine +participant "Rule(s)" as rule + +== Transfer == +token -> engine : canTransfer(from, to, amount) read-only pre-check +engine -> rule : detectTransferRestriction(from, to, amount) +rule --> engine : code (0 = TRANSFER_OK) +engine --> token : allowed / blocked +token -> engine : transferred(from, to, amount) +engine -> rule : transferred(from, to, amount) + +== Mint == +token -> engine : created(to, amount) +engine -> engine : _transferred(address(0), to, amount) +engine -> rule : transferred(address(0), to, amount) + +== Burn == +token -> engine : destroyed(from, amount) +engine -> engine : _transferred(from, address(0), amount) +engine -> rule : transferred(from, address(0), amount) + +note over token, rule + A rule alone implements only canTransfer + transferred from ICompliance. + It does NOT implement created / destroyed, so it cannot back an + ERC-3643 token directly — mint/burn compliance would be missed. + Use the RuleEngine, which implements the whole ICompliance interface. +end note +@enduml diff --git a/doc/img/readme-erc721-erc1155-compliance.png b/doc/img/readme-erc721-erc1155-compliance.png new file mode 100644 index 0000000..674715f Binary files /dev/null and b/doc/img/readme-erc721-erc1155-compliance.png differ diff --git a/doc/img/readme-erc721-erc1155-compliance.puml b/doc/img/readme-erc721-erc1155-compliance.puml new file mode 100644 index 0000000..92e9f8d --- /dev/null +++ b/doc/img/readme-erc721-erc1155-compliance.puml @@ -0,0 +1,35 @@ +@startuml +title ERC-721 / ERC-1155 compliance (IERC7943NonFungibleComplianceExtend) + +actor "Operator / Holder" as caller +participant "ERC-721 / ERC-1155\ntoken" as token +participant "Validation rule\n(RuleNFTAdapter)" as rule + +caller -> token : safeTransferFrom(from, to, tokenId[, amount]) + +note over token, rule + A single transferred(...) call both validates AND reverts: + internally it runs detectTransferRestrictionFrom and + require(code == TRANSFER_OK). No separate pre-check is needed + in the transfer path. +end note + +token -> rule : transferred(spender, from, to, tokenId, value) (in the token's transfer hook) +rule -> rule : code = detectTransferRestrictionFrom(spender, from, to, value)\nrequire(code == TRANSFER_OK) +alt restricted (code > 0) + rule --> token : revert + token --> caller : revert (whole transfer rolls back) +else allowed (code == 0) + rule --> token : ok + token --> caller : Transfer succeeds +end + +note over token, rule + amount / value == 1 for ERC-721. RuleNFTAdapter carries tokenId + but delegates to the address-based checks (from / to / spender). + The read-only detectTransferRestriction* / canTransfer* overloads + stay available for off-chain or pre-transfer queries. + Only validation rules implement this interface; operation rules + (e.g. RuleConditionalTransferLight) and RuleMaxTotalSupply are ERC-20 only. +end note +@enduml diff --git a/doc/img/rule-blacklist-flow.png b/doc/img/rule-blacklist-flow.png new file mode 100644 index 0000000..c16c67e Binary files /dev/null and b/doc/img/rule-blacklist-flow.png differ diff --git a/doc/img/rule-blacklist-flow.puml b/doc/img/rule-blacklist-flow.puml new file mode 100644 index 0000000..ff1370c --- /dev/null +++ b/doc/img/rule-blacklist-flow.puml @@ -0,0 +1,38 @@ +@startuml +title RuleBlacklist — transfer flow with a CMTAT token + +actor "Spender / Holder" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleBlacklist\n(blocked addresses)" as rule + +caller -> cmtat : transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> rule : detectTransferRestrictionFrom(spender, from, to, value) + note right of rule + isBlacklisted(from)? + isBlacklisted(to)? + isBlacklisted(spender)? (transferFrom) + end note + alt from blacklisted + rule --> cmtat : 36 (CODE_ADDRESS_FROM_IS_BLACKLISTED) + else to blacklisted + rule --> cmtat : 37 (CODE_ADDRESS_TO_IS_BLACKLISTED) + else spender blacklisted + rule --> cmtat : 38 (CODE_ADDRESS_SPENDER_IS_BLACKLISTED) + else none blacklisted + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(code) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-conditional-transfer-flow.png b/doc/img/rule-conditional-transfer-flow.png new file mode 100644 index 0000000..7484f59 Binary files /dev/null and b/doc/img/rule-conditional-transfer-flow.png differ diff --git a/doc/img/rule-conditional-transfer-flow.puml b/doc/img/rule-conditional-transfer-flow.puml new file mode 100644 index 0000000..66fe565 --- /dev/null +++ b/doc/img/rule-conditional-transfer-flow.puml @@ -0,0 +1,49 @@ +@startuml +title RuleConditionalTransfer (Vinkulierung) — request/approve/transfer flow with a CMTAT token + +actor "Token holder" as holder +actor "Rule operator" as op +participant "CMTAT token\n(bound to the rule)" as cmtat +participant "RuleEngine" as engine +participant "RuleConditionalTransfer\nrequests[key(from,to,value)]" as rule + +== 1. Request (holder or operator) == +holder -> rule : createTransferRequest(to, value) +rule -> rule : status = WAIT (request created) + +== 2. Approval (operator) == +op -> rule : approveTransferRequest(from, to, value) +rule -> rule : status = APPROVED +note right of rule + Options (set by operator): + - AUTOMATIC_APPROVAL: WAIT is treated as + approved after timeLimitBeforeAutomaticApproval + - TIME_LIMIT: request/transfer deadlines + - Conditional whitelist: from & to whitelisted + are auto-authorized without a request +end note + +alt AUTOMATIC_TRANSFER enabled (and rule has ERC-20 allowance) + rule -> cmtat : transferFrom(from, to, value) + note right of rule : rule performs the transfer itself on approval +end + +== 3. Transfer via CMTAT (manual case) == +holder -> cmtat : transfer(to, value) + +group Validation + operation + cmtat -> engine : detectTransferRestriction(from, to, value) + engine -> rule : detectTransferRestriction(from, to, value) + alt no APPROVED request (or deadline passed) + rule --> cmtat : 46 (request not approved) + cmtat --> holder : revert CMTAT_InvalidTransfer(46) + else request APPROVED and within time limit + rule --> cmtat : 0 (TRANSFER_OK) + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(from, to, value) + engine -> rule : transferred(...) + rule -> rule : status = EXECUTED (state change) + cmtat --> holder : Transfer(from, to, value) + end +end +@enduml diff --git a/doc/img/rule-conditional-transfer-light-flow.png b/doc/img/rule-conditional-transfer-light-flow.png new file mode 100644 index 0000000..0fff01d Binary files /dev/null and b/doc/img/rule-conditional-transfer-light-flow.png differ diff --git a/doc/img/rule-conditional-transfer-light-flow.puml b/doc/img/rule-conditional-transfer-light-flow.puml new file mode 100644 index 0000000..0183cdb --- /dev/null +++ b/doc/img/rule-conditional-transfer-light-flow.puml @@ -0,0 +1,37 @@ +@startuml +title RuleConditionalTransferLight — transfer flow with a CMTAT token (operation rule) + +actor "Operator" as op +actor "Holder / Spender" as caller +participant "CMTAT token\n(bound to the rule)" as cmtat +participant "RuleEngine" as engine +participant "RuleConditionalTransferLight\napprovalCounts[hash(from,to,value)]" as rule + +== Phase 1: approval (off the transfer path) == +op -> rule : approveTransfer(from, to, value) +rule -> rule : approvalCounts[hash]++ \n(state change) + +== Phase 2: the transfer == +caller -> cmtat : transfer(to, value) / transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestriction[From](... from, to, value) + engine -> rule : detectTransferRestriction[From](...) + note right of rule : approvalCounts[hash(from,to,value)] > 0 ? + alt no approval recorded + rule --> cmtat : 46 (CODE_TRANSFER_REQUEST_NOT_APPROVED) + else approval exists + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(46) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) + rule -> rule : require(count != 0); approvalCounts[hash]-- \n(consume one approval) + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-conditional-transfer-light-multitoken-flow.png b/doc/img/rule-conditional-transfer-light-multitoken-flow.png new file mode 100644 index 0000000..9b08a08 Binary files /dev/null and b/doc/img/rule-conditional-transfer-light-multitoken-flow.png differ diff --git a/doc/img/rule-conditional-transfer-light-multitoken-flow.puml b/doc/img/rule-conditional-transfer-light-multitoken-flow.puml new file mode 100644 index 0000000..a1bdefd --- /dev/null +++ b/doc/img/rule-conditional-transfer-light-multitoken-flow.puml @@ -0,0 +1,42 @@ +@startuml +title RuleConditionalTransferLightMultiToken — transfer flow with a CMTAT token (operation rule) + +actor "Operator" as op +actor "Holder / Spender" as caller +participant "CMTAT token\n(one of several bound)" as cmtat +participant "RuleEngine" as engine +participant "RuleConditionalTransferLightMultiToken\napprovalCounts[hash(token,from,to,value)]" as rule + +== Phase 1: approval (token-scoped) == +op -> rule : approveTransfer(token, from, to, value) +rule -> rule : approvalCounts[hash(token,...)]++ \n(state change) +note right of rule + Approval key includes the token address, + so one token cannot consume another + token's approvals. +end note + +== Phase 2: the transfer == +caller -> cmtat : transfer(to, value) / transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestriction[From](... from, to, value) + engine -> rule : detectTransferRestriction[From](...) token = msg.sender context + note right of rule : approvalCounts[hash(token,from,to,value)] > 0 ? + alt no approval for (token, from, to, value) + rule --> cmtat : 46 (CODE_TRANSFER_REQUEST_NOT_APPROVED) + else approval exists + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(46) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) + rule -> rule : require(count != 0); approvalCounts[hash(token,...)]-- \n(consume one approval) + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-erc2980-flow.png b/doc/img/rule-erc2980-flow.png new file mode 100644 index 0000000..fa161b8 Binary files /dev/null and b/doc/img/rule-erc2980-flow.png differ diff --git a/doc/img/rule-erc2980-flow.puml b/doc/img/rule-erc2980-flow.puml new file mode 100644 index 0000000..fc03b4f --- /dev/null +++ b/doc/img/rule-erc2980-flow.puml @@ -0,0 +1,41 @@ +@startuml +title RuleERC2980 — transfer flow with a CMTAT token + +actor "Spender / Holder" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleERC2980\n(frozenlist + whitelist)" as rule + +caller -> cmtat : transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> rule : detectTransferRestrictionFrom(spender, from, to, value) + note right of rule + Frozenlist is checked FIRST and blocks + sender, recipient and spender. + Whitelist only gates the recipient + (senders may freely spend held tokens). + end note + alt from frozen + rule --> cmtat : 60 (CODE_ADDRESS_FROM_IS_FROZEN) + else to frozen + rule --> cmtat : 61 (CODE_ADDRESS_TO_IS_FROZEN) + else spender frozen + rule --> cmtat : 62 (CODE_ADDRESS_SPENDER_IS_FROZEN) + else to not whitelisted + rule --> cmtat : 63 (CODE_ADDRESS_TO_NOT_WHITELISTED) + else not frozen and recipient whitelisted + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(code) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-identity-registry-flow.png b/doc/img/rule-identity-registry-flow.png new file mode 100644 index 0000000..e252cc3 Binary files /dev/null and b/doc/img/rule-identity-registry-flow.png differ diff --git a/doc/img/rule-identity-registry-flow.puml b/doc/img/rule-identity-registry-flow.puml new file mode 100644 index 0000000..8f0c51d --- /dev/null +++ b/doc/img/rule-identity-registry-flow.puml @@ -0,0 +1,41 @@ +@startuml +title RuleIdentityRegistry — transfer flow with a CMTAT token + +actor "Spender / Holder" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleIdentityRegistry" as rule +participant "ERC-3643\nIdentity Registry" as reg + +caller -> cmtat : transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> rule : detectTransferRestrictionFrom(spender, from, to, value) + alt registry == address(0) + note right of rule : no registry configured -> rule passes + rule --> cmtat : 0 (TRANSFER_OK) + else registry configured + rule -> reg : isVerified(from) / isVerified(to) / isVerified(spender) + reg --> rule : true / false + alt from not verified + rule --> cmtat : 55 (CODE_ADDRESS_FROM_NOT_VERIFIED) + else to not verified + rule --> cmtat : 56 (CODE_ADDRESS_TO_NOT_VERIFIED) + else spender not verified + rule --> cmtat : 57 (CODE_ADDRESS_SPENDER_NOT_VERIFIED) + else all verified + rule --> cmtat : 0 (TRANSFER_OK) + end + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(code) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-max-total-supply-flow.png b/doc/img/rule-max-total-supply-flow.png new file mode 100644 index 0000000..ca2e5c9 Binary files /dev/null and b/doc/img/rule-max-total-supply-flow.png differ diff --git a/doc/img/rule-max-total-supply-flow.puml b/doc/img/rule-max-total-supply-flow.puml new file mode 100644 index 0000000..a13f6ac --- /dev/null +++ b/doc/img/rule-max-total-supply-flow.puml @@ -0,0 +1,34 @@ +@startuml +title RuleMaxTotalSupply — mint flow with a CMTAT token + +actor "Minter" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleMaxTotalSupply\n(max cap on totalSupply)" as rule + +caller -> cmtat : mint(to, value) (from == address(0)) + +group Validation (read-only) + cmtat -> engine : detectTransferRestriction(address(0), to, value) + engine -> rule : detectTransferRestriction(address(0), to, value) + note right of rule + Only mints (from == 0) are checked. + Transfers between holders and burns pass. + Reads token.totalSupply(). + end note + alt totalSupply() + value > maxTotalSupply + rule --> cmtat : 50 (CODE_MAX_TOTAL_SUPPLY_EXCEEDED) + else within cap + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(50) +else code == 0 + cmtat -> cmtat : _update: mint value to `to` + cmtat -> engine : transferred(operator, address(0), to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(address(0), to, value) +end +@enduml diff --git a/doc/img/rule-mint-allowance-flow.png b/doc/img/rule-mint-allowance-flow.png new file mode 100644 index 0000000..b4a091b Binary files /dev/null and b/doc/img/rule-mint-allowance-flow.png differ diff --git a/doc/img/rule-mint-allowance-flow.puml b/doc/img/rule-mint-allowance-flow.puml new file mode 100644 index 0000000..5e58762 --- /dev/null +++ b/doc/img/rule-mint-allowance-flow.puml @@ -0,0 +1,35 @@ +@startuml +title RuleMintAllowance — mint flow with a CMTAT token (operation rule) + +actor "Minter\n(spender)" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleMintAllowance\n(per-minter quota)" as rule + +caller -> cmtat : mint(to, value) (from == address(0)) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(minter, address(0), to, value) + engine -> rule : detectTransferRestrictionFrom(minter, address(0), to, value) + note right of rule + Only mints are gated. Needs the minter + (spender-aware path); the 3-arg + detectTransferRestriction always returns OK. + end note + alt allowance[minter] < value + rule --> cmtat : 70 (CODE_MINTER_ALLOWANCE_EXCEEDED) + else allowance[minter] >= value + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(70) +else code == 0 + cmtat -> cmtat : _update: mint value to `to` + cmtat -> engine : transferred(minter, address(0), to, value) + engine -> rule : transferred(minter, address(0), to, value) + rule -> rule : allowance[minter] -= value\n(state change) + cmtat --> caller : Transfer(address(0), to, value) +end +@enduml diff --git a/doc/img/rule-sanctionslist-flow.png b/doc/img/rule-sanctionslist-flow.png new file mode 100644 index 0000000..ea87e3d Binary files /dev/null and b/doc/img/rule-sanctionslist-flow.png differ diff --git a/doc/img/rule-sanctionslist-flow.puml b/doc/img/rule-sanctionslist-flow.puml new file mode 100644 index 0000000..d983be0 --- /dev/null +++ b/doc/img/rule-sanctionslist-flow.puml @@ -0,0 +1,41 @@ +@startuml +title RuleSanctionsList — transfer flow with a CMTAT token + +actor "Spender / Holder" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleSanctionsList" as rule +participant "Chainalysis oracle\n(SanctionsList)" as oracle + +caller -> cmtat : transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> rule : detectTransferRestrictionFrom(spender, from, to, value) + alt oracle == address(0) + note right of rule : no oracle configured -> rule passes + rule --> cmtat : 0 (TRANSFER_OK) + else oracle configured + rule -> oracle : isSanctioned(from) / isSanctioned(to) / isSanctioned(spender) + oracle --> rule : true / false + alt from sanctioned + rule --> cmtat : 30 (CODE_ADDRESS_FROM_IS_SANCTIONED) + else to sanctioned + rule --> cmtat : 31 (CODE_ADDRESS_TO_IS_SANCTIONED) + else spender sanctioned + rule --> cmtat : 32 (CODE_ADDRESS_SPENDER_IS_SANCTIONED) + else none sanctioned + rule --> cmtat : 0 (TRANSFER_OK) + end + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(code) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-spender-whitelist-flow.png b/doc/img/rule-spender-whitelist-flow.png new file mode 100644 index 0000000..72d1005 Binary files /dev/null and b/doc/img/rule-spender-whitelist-flow.png differ diff --git a/doc/img/rule-spender-whitelist-flow.puml b/doc/img/rule-spender-whitelist-flow.puml new file mode 100644 index 0000000..4bb71f0 --- /dev/null +++ b/doc/img/rule-spender-whitelist-flow.puml @@ -0,0 +1,35 @@ +@startuml +title RuleSpenderWhitelist — transfer flow with a CMTAT token + +actor "Caller" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleSpenderWhitelist\n(spender whitelist)" as rule + +alt direct transfer(to, value) (no spender) + caller -> cmtat : transfer(to, value) + cmtat -> engine : detectTransferRestriction(from, to, value) + engine -> rule : detectTransferRestriction(from, to, value) + note right of rule : direct transfers are always accepted + rule --> cmtat : 0 (TRANSFER_OK) +else transferFrom(from, to, value) (spender-initiated) + caller -> cmtat : transferFrom(from, to, value) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> rule : detectTransferRestrictionFrom(spender, from, to, value) + note right of rule : isWhitelisted(spender)? + alt spender not whitelisted + rule --> cmtat : 66 (CODE_ADDRESS_SPENDER_NOT_WHITELISTED) + else spender whitelisted + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(66) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-whitelist-flow.png b/doc/img/rule-whitelist-flow.png new file mode 100644 index 0000000..3db0ed9 Binary files /dev/null and b/doc/img/rule-whitelist-flow.png differ diff --git a/doc/img/rule-whitelist-flow.puml b/doc/img/rule-whitelist-flow.puml new file mode 100644 index 0000000..4c2e44e --- /dev/null +++ b/doc/img/rule-whitelist-flow.puml @@ -0,0 +1,38 @@ +@startuml +title RuleWhitelist — transfer flow with a CMTAT token + +actor "Spender / Holder" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleWhitelist\n(from/to whitelist,\noptional checkSpender)" as rule + +caller -> cmtat : transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> rule : detectTransferRestrictionFrom(spender, from, to, value) + note right of rule + isWhitelisted(from)? + isWhitelisted(to)? + if checkSpender: isWhitelisted(spender)? + end note + alt from not whitelisted + rule --> cmtat : 21 (CODE_ADDRESS_FROM_NOT_WHITELISTED) + else to not whitelisted + rule --> cmtat : 22 (CODE_ADDRESS_TO_NOT_WHITELISTED) + else spender not whitelisted (checkSpender on) + rule --> cmtat : 23 (CODE_ADDRESS_SPENDER_NOT_WHITELISTED) + else all whitelisted + rule --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(code) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> rule : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/img/rule-whitelist-wrapper-flow.png b/doc/img/rule-whitelist-wrapper-flow.png new file mode 100644 index 0000000..3d57484 Binary files /dev/null and b/doc/img/rule-whitelist-wrapper-flow.png differ diff --git a/doc/img/rule-whitelist-wrapper-flow.puml b/doc/img/rule-whitelist-wrapper-flow.puml new file mode 100644 index 0000000..0c200b1 --- /dev/null +++ b/doc/img/rule-whitelist-wrapper-flow.puml @@ -0,0 +1,44 @@ +@startuml +title RuleWhitelistWrapper — transfer flow with a CMTAT token + +actor "Spender / Holder" as caller +participant "CMTAT token" as cmtat +participant "RuleEngine" as engine +participant "RuleWhitelistWrapper\n(OR over child rules,\noptional checkSpender)" as wrap +participant "Child whitelist rules\n(IAddressList)" as child + +caller -> cmtat : transferFrom(from, to, value) + +group Validation (read-only) + cmtat -> engine : detectTransferRestrictionFrom(spender, from, to, value) + engine -> wrap : detectTransferRestrictionFrom(spender, from, to, value) + note right of wrap + An address is whitelisted if ANY + child rule lists it (OR logic). + Iteration stops early once all + required addresses are resolved. + end note + loop over registered child rules until resolved + wrap -> child : isWhitelisted(from) / isWhitelisted(to) [/ spender] + child --> wrap : true / false + end + alt from listed in no child + wrap --> cmtat : 21 (CODE_ADDRESS_FROM_NOT_WHITELISTED) + else to listed in no child + wrap --> cmtat : 22 (CODE_ADDRESS_TO_NOT_WHITELISTED) + else spender listed in no child (checkSpender on) + wrap --> cmtat : 23 (CODE_ADDRESS_SPENDER_NOT_WHITELISTED) + else all resolved in some child + wrap --> cmtat : 0 (TRANSFER_OK) + end +end + +alt restriction code != 0 + cmtat --> caller : revert CMTAT_InvalidTransfer(code) +else code == 0 + cmtat -> cmtat : _update: move balances + cmtat -> engine : transferred(spender, from, to, value) + engine -> wrap : transferred(...) no state change + cmtat --> caller : Transfer(from, to, value) +end +@enduml diff --git a/doc/security/audits/AUDIT_OVERVIEW.md b/doc/security/audits/AUDIT_OVERVIEW.md new file mode 100644 index 0000000..79678fe --- /dev/null +++ b/doc/security/audits/AUDIT_OVERVIEW.md @@ -0,0 +1,69 @@ +# Audit & Security-Analysis Overview + +> This is a security **overview** (analyses index + triage). It is **not** the vulnerability-reporting policy +> (that belongs in a root `SECURITY.md`). + +**Current package version:** `v0.4.0` +**Scope:** production contracts under `src/` — mocks/tests (`src/mocks`, `test/`) and dependencies (`lib/`) are excluded from static-analysis runs unless a run is explicitly marked *mocks included*. + +> ⚠️ This project has **not** undergone a formal third-party security audit. The analyses below are automated +> static analysis plus AI-assisted review, with the project team's triage. + +## Analyses + +| Date | Type | Tool / Source | Version | Reports | +|---|---|---|---|---| +| 2026-07 | AI-assisted review | Claude (Anthropic) + custom security-audit skills | v0.4.0 | [**CLAUDE_AUDIT.md**](./tools/v0.4.0/claude-audit/CLAUDE_AUDIT.md) | +| 2026-07-14 | Static analysis | Slither 0.11.5 | v0.4.0 | [report](./tools/v0.4.0/slither-report.md) · [feedback](./tools/v0.4.0/slither-report-feedback.md) | +| 2026-07-14 | Static analysis | Aderyn 0.6.5 | v0.4.0 | [report](./tools/v0.4.0/aderyn-report.md) · [feedback](./tools/v0.4.0/aderyn-report-feedback.md) | +| 2026-04-16 | Static analysis | Slither / Aderyn | v0.3.0 | [slither](./tools/v0.3.0/slither-report.md) · [aderyn](./tools/v0.3.0/aderyn-report.md) | +| 2026-03-16 | AI-assisted review | Wake Arena (Ackee) | v0.2.0 | [tools/v0.2.0](./tools/v0.2.0/) | + +## Static-analysis results (v0.4.0) + +Both tools were **re-run on 2026-07-14**, after the security remediation landed. Counts below are from that run. + +| Tool | High | Medium | Low | Info | Relevant to fix? | +|---|---|---|---|---|---| +| Slither 0.11.5 | 2 | 6 | 16 | 12 | **No** — all false-positive or by-design ([feedback](./tools/v0.4.0/slither-report-feedback.md)) | +| Aderyn 0.6.5 | 0 | 0 | 9 findings | 0 | **No** — all Low, by-design or false-positive ([feedback](./tools/v0.4.0/aderyn-report-feedback.md)) | + +**Result: nothing to fix in `v0.4.0`.** The two High-severity Slither `arbitrary-send-erc20` hits are false positives — `approveAndTransferIfAllowed` (light + multi-token variants) is gated by `onlyTransferApprover`, a recorded approval, an allowance check, and a bound token. All other findings are accepted by design (centralization, unspecific pragma, PUSH0, template-method modifiers/empty blocks, `EnumerableSet` loop cost, spec-aligned naming) or tool limitations (`unused-return`/`unused-state`, per-contract analysis). + +Slither's tally is **unchanged** from the pre-remediation run. Aderyn moved 8 → 10 Low, of which one was real and was fixed, leaving 9: + +- **L-7 `Loop Contains require/revert` (new, 3 instances)** — batch adds now revert on `address(0)` instead of skipping it. Aderyn recommends "forgive on fail and continue", which is exactly the behaviour this release removed: a silent skip left the emitted `AddAddresses` event naming the sentinel as a set member when it was not. **The recommendation is deliberately rejected; do not act on it.** +- **`Unused Import` (2 instances) — found and FIXED during this run.** A dead `RuleTransferValidation` import in the two `RuleSpenderWhitelist` deployment files (pre-existing, not a regression). It was the only actionable item across both tools; both imports were removed, the build is clean, 511 tests pass, and a re-run confirms the finding is gone. + +## AI-assisted review results (v0.4.0) + +**0 Critical · 0 High · 0 Medium · 2 Low · 8 Informational** — plus 8 observations verified safe or accepted by design. Full detail, including invariant and access-control verification, in [`CLAUDE_AUDIT.md`](./tools/v0.4.0/claude-audit/CLAUDE_AUDIT.md). + +| ID | Severity | Finding | Status | +|---|---|---|---| +| F-1 | **Low** | `RuleIdentityRegistry` over-screens vs ERC-3643: it verified sender, spender and minter, where the spec mandates the **receiver only**. Blocked issuance when the minter was unregistered, and **trapped de-listed holders** (they could neither receive nor send). | ✅ **Fixed** — conformant by default; stricter checks moved behind opt-in flags *(breaking)* | +| F-4 | **Low** | `RuleConditionalTransferLightMultiToken` keys approvals by `msg.sender`, not by `token`, so behind a `RuleEngine` no wiring delivers per-token isolation. | ⚠️ **Documented** — rule declared direct-binding-only; code unchanged (a true fix needs an upstream `RuleEngine` interface change) | +| F-2 | Info | Supply-cap restriction views panic on overflow instead of returning code `50`. | ✅ **Fixed** — overflow-safe views | +| F-3 | Info | `approveAndTransferIfAllowed` was inoperable behind a `RuleEngine` (`bindToken` conflated the ERC-20 target with the authorized caller). | ✅ **Fixed** — `bindRuleEngine` splits the two roles | +| F-5 | Info | The whitelist wrapper does not ERC-165-check its child rules. | ⚙️ **Partially fixed** — `IAddressList` now advertised; the wrapper guard remains open | +| F-7 | Info | `RuleMintAllowance.canTransfer` is hardcoded to "allowed" and disagrees with enforcement. | ⚠️ **By design** — documented as non-authoritative | +| F-8 | Info | Multi-token `detectTransferRestriction` depends on `msg.sender`, so third-party pre-flight always reads "not approved". | ✅ **Fixed** — caller-explicit `…ForToken` views | +| F-9 | Info | `unbindToken` leaves stale approvals / mint quota. | ✅ **Mitigated** — `resetApproval` / `clearMintAllowances` added | +| F-10, F-14 | Info | Multi-token doc contradicted itself on approval scoping; project guide stale. | ✅ **Fixed** (documentation) | + +Additionally, **two standards-conformance defects were fixed** that were not in the original finding list: enabling mint/burn by whitelisting `address(0)` made `isVerified(address(0))` (ERC-3643) and `whitelist(address(0))` (a **mandatory** ERC-2980 getter) return `true`. Mint/burn permission is now an explicit `allowMint` / `allowBurn` flag and the zero address can never enter a list *(breaking)*. + +## Substantive findings that were fixed (AI / manual review) + +From the Wake Arena AI review (v0.2.0) and internal `RuleMintAllowance` review: + +| Source | ID | Finding | Resolution | +|---|---|---|---| +| Wake Arena | H-1 | ConditionalTransferLight approvals not scoped by token | **Fixed** — single-token binding enforced in `bindToken`; `RuleConditionalTransferLight_TokenAlreadyBound` added. | +| Wake Arena | M-1 | Incomplete `supportsInterface` breaks ERC-165 discovery | **Fixed** — pre-computed interface IDs + `IERC7551Compliance`; full ERC-3643 `ICompliance` ID (`0x3144991c`) handled. | +| Wake Arena | I-1 | RuleERC2980 docs omit frozen spender on `transferFrom` | **Fixed (doc)** — README / AGENTS / CLAUDE updated. | +| Wake Arena | I-2 | `hasRole` admin implicitly passes all role checks | **Fixed (doc)** — documented intentional design + off-chain monitoring guidance. | +| Internal review | — | `RuleMintAllowance` allowance shared across bindings | **Fixed** — single-target binding enforced (`RuleMintAllowance_TokenAlreadyBound`). | +| Internal review | — | `SanctionListOracle` mock `removeFromSanctionsList` set `true` instead of `false` | **Fixed** — corrected to un-sanction; regression test added (mock/test-only). | + +See the per-version report directories under [`tools/`](./tools/) for the full outputs and triage. diff --git a/doc/security/audits/tools/v0.4.0/aderyn-report-feedback.md b/doc/security/audits/tools/v0.4.0/aderyn-report-feedback.md new file mode 100644 index 0000000..c3c32f2 --- /dev/null +++ b/doc/security/audits/tools/v0.4.0/aderyn-report-feedback.md @@ -0,0 +1,181 @@ +# Aderyn Report — Feedback + +Report version: `v0.4.0` +Aderyn report: [aderyn-report.md](./aderyn-report.md) +Tool: Aderyn 0.6.5 · Scope: production contracts only, 64 files, 3 087 nSLOC (**mocks excluded**) +Feedback date: 2026-07-14 (re-run after the v0.4.0 security remediation) + +This document provides the project team's assessment of each finding reported by the Aderyn static analyser. Verdicts: + +| Verdict | Meaning | +|---|---| +| **Acknowledged** | Known, accepted by design; no change planned. | +| **By design** | Behaviour is intentional and required by the architecture. | +| **Fixed** | Resolved in the codebase. | +| **To fix** | Will be addressed in a future revision. | +| **False positive** | Tool mis-identification; no real issue exists. | + +All 9 remaining findings are **Low** severity. No High or Medium issues were reported. + +The first pass of this run reported **10** Low findings. One of them — `Unused Import` — was a genuine (if cosmetic) +defect and has been **fixed**; the report was regenerated afterwards, so it now shows 9. See +[Fixed during this run](#fixed-during-this-run-unused-import) below. + +> ⚠️ **Renumbering.** One new category appeared in this run, so the IDs shifted relative to the 2026-07-08 report. +> Previous `L-7 Costly operations inside loop` is now **L-8**; previous `L-8 Unchecked Return` is now **L-9**. +> The new category is **L-7 (Loop Contains `require`/`revert`)**. + +--- + +## L-1: Centralization Risk — 68 instances + +**Verdict: By design** + +This library implements compliance rules for regulated security tokens (CMTAT / ERC-3643). Admin and operator roles are intentionally held by the issuer/compliance operator — an explicit trust assumption, not a bug. The premise is stated plainly in [`CLAUDE_AUDIT.md`](./claude-audit/CLAUDE_AUDIT.md) §5: the default admin implicitly holds every role, so role separation limits blast radius between honest operators, not against the admin itself. + +Count rose 62 → 68: the new mint/burn flag setters (`setAllowMint` / `setAllowBurn`), the identity-registry check flags (`setCheckSender` / `setCheckSpender`), and the new binding/reset functions (`bindRuleEngine`, `resetApproval`, `clearMintAllowances`) are all role-gated. + +--- + +## L-2: Unspecific Solidity Pragma — 63 instances + +**Verdict: By design** + +The repository intentionally uses `pragma solidity ^0.8.20` to stay integrator-friendly, while the project itself is built deterministically via `foundry.toml` (`solc = 0.8.34`). + +--- + +## L-3: Address State Variable Set Without Checks — 1 instance + +**Verdict: False positive** + +Flagged location: `RuleSanctionsListBase._setSanctionListOracle` (`src/rules/validation/abstract/base/RuleSanctionsListBase.sol#L125`). + +The zero-address guard is enforced at the public boundary `setSanctionListOracle(...)` (reverts with `RuleSanctionsList_OracleAddressZeroNotAllowed`). The internal setter accepts `address(0)` intentionally because `clearSanctionListOracle()` must disable the oracle. + +--- + +## L-4: PUSH0 Opcode — 64 instances + +**Verdict: By design — not applicable** + +The project targets `evm_version = "prague"` in `foundry.toml`; deployment targets are expected to support Shanghai+ opcodes including `PUSH0`. + +--- + +## L-5: Modifier Invoked Only Once — 2 instances + +**Verdict: By design (template method pattern)** + +Flagged modifiers are deliberate authorization wrappers over abstract `_authorize*` hooks. Inlining would weaken consistency across the AccessControl and Ownable variants. + +--- + +## L-6: Empty Block — 61 instances + +**Verdict: By design (template method pattern / interface compliance)** + +Most empty blocks are `_authorize*` hook implementations where the check is provided by modifiers (`onlyRole`, `onlyOwner`). Others are intentional no-ops required by shared interfaces in rules that are read-only for specific paths. + +--- + +## L-7: Loop Contains `require`/`revert` — 3 instances — **NEW** + +**Verdict: By design — the tool's recommendation is explicitly rejected** + +Flagged locations: +- `RuleAddressSetInternal.sol#L42` (`_addAddresses`) +- `RuleERC2980Internal.sol#L48` (`_addWhitelistAddresses`) +- `RuleERC2980Internal.sol#L109` (`_addFrozenlistAddresses`) + +All three are the **same deliberate change**, introduced in this release: a batch add now reverts on `address(0)` rather than skipping it. + +Aderyn's advice is *"better to forgive on fail and return failed elements post processing of the loop"* — i.e. skip the bad item and continue. **That is precisely the behaviour this release removed, and removing it was the point.** The zero address is the mint/burn sentinel, not a list member. When the batch silently skipped it, the function still emitted `AddAddresses` / `AddWhitelistAddresses` / `AddFrozenlistAddresses` naming `address(0)` — so the event told every off-chain indexer that the sentinel was a member of the set when it was not. Reverting is what keeps the emitted event truthful. + +The non-reverting-batch convention still holds for **duplicates**, which are an idempotent no-op the event describes accurately. The zero address is the one input a batch does not forgive. See the `v0.4.0` CHANGELOG entry and [`CLAUDE_AUDIT.md`](./claude-audit/CLAUDE_AUDIT.md) §6.2. + +--- + +## L-8: Costly operations inside loop — 7 instances + +**Verdict: By design — unavoidable** + +Flagged loops perform `EnumerableSet` insert/remove operations across batch APIs. These are inherently storage writes (`SSTORE`) per item, so linear gas growth is expected and unavoidable. (Was L-7 in the previous report; 6 → 7 instances.) + +--- + +## Fixed during this run: Unused Import — 2 instances + +**Verdict: Fixed** — the only finding in this report that warranted a code change. + +Flagged locations (first pass, `L-9`): +- `src/rules/validation/deployment/RuleSpenderWhitelist.sol#L9` +- `src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L8` + +Both were `import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol";`. **Verified against the source before acting: the symbol was referenced nowhere else in either file** — not in the inheritance list, not in an `override(...)` specifier, not in a function body. The sibling `RuleWhitelist.sol` does not import it at all, which confirmed the deployment variants never needed it. + +Both imports were removed. `forge build` is clean and the full suite passes (506 tests, 78 suites). A re-run of Aderyn confirms the finding is gone, taking the total from 10 Low to **9 Low**. + +No security impact — an unused import affects neither bytecode nor behaviour — but it was dead code, and removing it keeps the tool's signal clean for the next run. + +--- + +## L-9: Unchecked Return — 13 instances + +**Verdict: Mixed — majority false positives** + +| Instance | Assessment | +|---|---| +| `_grantRole(DEFAULT_ADMIN_ROLE, admin)` in `AccessControlModuleStandalone` | **Acknowledged / low impact** — constructor path; a duplicate grant returns `false` and is not expected on fresh deployment. | +| `_addAddresses(...)` / `_removeAddresses(...)` batch helpers | **False positive** — `void` helpers, no return value to check. | +| `_listedAddresses.add/remove` in `RuleAddressSetInternal` single-item helpers | **False positive** — correctness guaranteed by outer pre-checks in public single-item methods. | +| `_whitelist.add/remove` and `_frozenlist.add/remove` in `RuleERC2980Internal` single-item helpers | **False positive** — same pre-check pattern. | +| batch `_add*Addresses` / `_remove*Addresses` helpers | **False positive** — no unchecked boolean return at the API boundary. | + +(Was L-8 in the previous report; unchanged at 13 instances.) + +--- + +## Delta from the 2026-07-08 v0.4.0 run + +Same tool version (0.6.5), same scope. All movement is caused by the security remediation landed in this release, not by a tool change. + +| ID (now) | Finding | 2026-07-08 | 2026-07-14 | Cause of change | +|---|---|---|---|---| +| L-1 | Centralization Risk | 62 | **68** | New role-gated setters (mint/burn flags, identity check flags, `bindRuleEngine`, `resetApproval`, `clearMintAllowances`) | +| L-2 | Unspecific Pragma | 63 | 63 | — | +| L-3 | Address Set Without Checks | 1 | 1 | — | +| L-4 | PUSH0 Opcode | 63 | **64** | One new source file (`AddressListInterfaceId`) | +| L-5 | Modifier Invoked Once | 2 | 2 | — | +| L-6 | Empty Block | 55 | **61** | New `_authorize*` hook overrides for the added setters | +| L-7 | Loop Contains `require`/`revert` | — | **3 (new)** | Batch adds now revert on `address(0)` — intended | +| L-8 | Costly ops in loop | 6 | **7** | — | +| L-9 | Unchecked Return | 13 | 13 | — | +| — | Unused Import | — | **0** (2 found, then **fixed**) | Dead `RuleTransferValidation` import removed during this run | + +Totals: **8 Low → 10 Low as first reported → 9 Low after the Unused Import fix.** 0 High, 0 Medium throughout. + +Note that Unused Import was not a regression introduced by this release — the import was already dead. Aderyn 0.6.5 surfaced it now because the surrounding files changed, bringing them back into its analysis path. It has since been removed. + +## Executive triage + +**Nothing left to fix.** All 9 remaining findings are Low severity, and none is exploitable. + +- **The one actionable item, `Unused Import`, has been fixed** — the dead `RuleTransferValidation` import is gone from both `RuleSpenderWhitelist` deployment files. Build clean, 506 tests passing, and a re-run confirms the finding no longer appears. +- **L-7 is a false alarm in the strong sense:** the tool recommends reverting a change that was made deliberately, for a documented correctness reason. Do not "fix" it. A maintainer who acts on Aderyn's advice here would reintroduce an event that misreports the zero address as a list member. +- Everything else is by design (centralization, pragma, PUSH0, template-method modifiers/empty blocks, `EnumerableSet` loop cost) or a false positive (L-3, and the majority of L-9). + +## Summary + +| ID | Title | Instances | Verdict | +|---|---|---|---| +| L-1 | Centralization Risk | 68 | By design | +| L-2 | Unspecific Solidity Pragma | 63 | By design | +| L-3 | Address State Variable Set Without Checks | 1 | False positive | +| L-4 | PUSH0 Opcode | 64 | By design — not applicable | +| L-5 | Modifier Invoked Only Once | 2 | By design | +| L-6 | Empty Block | 61 | By design | +| L-7 | Loop Contains `require`/`revert` | 3 | **By design — recommendation rejected** | +| L-8 | Costly operations inside loop | 7 | By design — unavoidable | +| L-9 | Unchecked Return | 13 | Mixed (majority false positives) | +| — | Unused Import | 0 | **Fixed** during this run (was 2) | diff --git a/doc/security/audits/tools/v0.4.0/aderyn-report.md b/doc/security/audits/tools/v0.4.0/aderyn-report.md new file mode 100644 index 0000000..1eaaa4c --- /dev/null +++ b/doc/security/audits/tools/v0.4.0/aderyn-report.md @@ -0,0 +1,1934 @@ +# Aderyn Report — `v0.4.0` + +```bash +aderyn -x mocks --output doc/security/audits/tools/v0.4.0/aderyn-report.md +``` + +Tool: **Aderyn 0.6.5** · Scope: production contracts only, 64 `.sol` files, 3 087 nSLOC (**mocks excluded**) +Run date: 2026-07-14 (re-run after the v0.4.0 security remediation) + +**Result: 0 High · 9 Low · 0 Medium · 0 Info.** + +| ID | Severity | Instances | Assessment | +|---|---|---|---| +| L-1 Centralization Risk | Low | 68 | By design — issuer/compliance operator is a trusted actor | +| L-2 Unspecific Solidity Pragma | Low | 63 | By design — `^0.8.20` for integrators; built with pinned `solc 0.8.34` | +| L-3 Address State Variable Set Without Checks | Low | 1 | False positive — guard is at the public boundary | +| L-4 PUSH0 Opcode | Low | 64 | By design — targets `evm_version = "prague"` | +| L-5 Modifier Invoked Only Once | Low | 2 | By design — template-method authorization wrappers | +| L-6 Empty Block | Low | 61 | By design — `_authorize*` hooks guarded by modifiers | +| L-7 Loop Contains `require`/`revert` | Low | 3 | **By design — the tool's advice is explicitly rejected** (see feedback) | +| L-8 Costly operations inside loop | Low | 7 | By design — `EnumerableSet` writes are inherently per-item | +| L-9 Unchecked Return | Low | 13 | Mixed — majority false positives | + +**Nothing to fix.** Every remaining finding is by-design or a false positive — triaged individually in the feedback file. + +The one actionable item from the first pass of this run, `Unused Import` (a dead `RuleTransferValidation` import in the two `RuleSpenderWhitelist` deployment files), has been **fixed**; this report reflects the code after that removal. + +Full triage: [aderyn-report-feedback.md](./aderyn-report-feedback.md) · Overview: [AUDIT_OVERVIEW.md](../../AUDIT_OVERVIEW.md) · Manual audit: [CLAUDE_AUDIT.md](./claude-audit/CLAUDE_AUDIT.md) + +--- + +# Aderyn Analysis Report + +This report was generated by [Aderyn](https://github.com/Cyfrin/aderyn), a static analysis tool built by [Cyfrin](https://cyfrin.io), a blockchain security company. This report is not a substitute for manual audit or security review. It should not be relied upon for any purpose other than to assist in the identification of potential security vulnerabilities. +# Table of Contents + +- [Summary](#summary) + - [Files Summary](#files-summary) + - [Files Details](#files-details) + - [Issue Summary](#issue-summary) +- [Low Issues](#low-issues) + - [L-1: Centralization Risk](#l-1-centralization-risk) + - [L-2: Unspecific Solidity Pragma](#l-2-unspecific-solidity-pragma) + - [L-3: Address State Variable Set Without Checks](#l-3-address-state-variable-set-without-checks) + - [L-4: PUSH0 Opcode](#l-4-push0-opcode) + - [L-5: Modifier Invoked Only Once](#l-5-modifier-invoked-only-once) + - [L-6: Empty Block](#l-6-empty-block) + - [L-7: Loop Contains `require`/`revert`](#l-7-loop-contains-requirerevert) + - [L-8: Costly operations inside loop](#l-8-costly-operations-inside-loop) + - [L-9: Unchecked Return](#l-9-unchecked-return) + + +# Summary + +## Files Summary + +| Key | Value | +| --- | --- | +| .sol Files | 64 | +| Total nSLOC | 3087 | + + +## Files Details + +| Filepath | nSLOC | +| --- | --- | +| src/modules/AccessControlModuleStandalone.sol | 24 | +| src/modules/MetaTxModuleStandalone.sol | 6 | +| src/modules/Ownable2StepERC165Module.sol | 11 | +| src/modules/VersionModule.sol | 8 | +| src/rules/interfaces/IAddressList.sol | 15 | +| src/rules/interfaces/IERC2980.sol | 5 | +| src/rules/interfaces/IERC7943NonFungibleCompliance.sol | 19 | +| src/rules/interfaces/IIdentityRegistry.sol | 7 | +| src/rules/interfaces/ISanctionsList.sol | 4 | +| src/rules/interfaces/ITotalSupply.sol | 4 | +| src/rules/interfaces/ITransferContext.sol | 22 | +| src/rules/interfaces/library/AddressListInterfaceId.sol | 4 | +| src/rules/operation/RuleConditionalTransferLight.sol | 40 | +| src/rules/operation/RuleConditionalTransferLightMultiToken.sol | 33 | +| src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol | 32 | +| src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol | 33 | +| src/rules/operation/RuleMintAllowance.sol | 34 | +| src/rules/operation/RuleMintAllowanceOwnable2Step.sol | 27 | +| src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol | 69 | +| src/rules/operation/abstract/RuleConditionalTransferLightBase.sol | 141 | +| src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol | 24 | +| src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol | 233 | +| src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol | 27 | +| src/rules/operation/abstract/RuleMintAllowanceBase.sol | 142 | +| src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol | 14 | +| src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol | 67 | +| src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol | 41 | +| src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol | 8 | +| src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol | 14 | +| src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol | 21 | +| src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol | 82 | +| src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol | 39 | +| src/rules/validation/abstract/base/RuleBlacklistBase.sol | 102 | +| src/rules/validation/abstract/base/RuleERC2980Base.sol | 251 | +| src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol | 126 | +| src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol | 90 | +| src/rules/validation/abstract/base/RuleSanctionsListBase.sol | 108 | +| src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol | 58 | +| src/rules/validation/abstract/base/RuleWhitelistBase.sol | 76 | +| src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol | 168 | +| src/rules/validation/abstract/core/RuleNFTAdapter.sol | 117 | +| src/rules/validation/abstract/core/RuleTransferValidation.sol | 69 | +| src/rules/validation/abstract/core/RuleWhitelistShared.sol | 84 | +| src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol | 18 | +| src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol | 13 | +| src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol | 18 | +| src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol | 4 | +| src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol | 9 | +| src/rules/validation/deployment/RuleBlacklist.sol | 33 | +| src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol | 31 | +| src/rules/validation/deployment/RuleERC2980.sol | 34 | +| src/rules/validation/deployment/RuleERC2980Ownable2Step.sol | 35 | +| src/rules/validation/deployment/RuleIdentityRegistry.sol | 22 | +| src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol | 23 | +| src/rules/validation/deployment/RuleMaxTotalSupply.sol | 22 | +| src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol | 23 | +| src/rules/validation/deployment/RuleSanctionsList.sol | 34 | +| src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol | 35 | +| src/rules/validation/deployment/RuleSpenderWhitelist.sol | 33 | +| src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol | 34 | +| src/rules/validation/deployment/RuleWhitelist.sol | 36 | +| src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol | 36 | +| src/rules/validation/deployment/RuleWhitelistWrapper.sol | 54 | +| src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol | 41 | +| **Total** | **3087** | + + +## Issue Summary + +| Category | No. of Issues | +| --- | --- | +| High | 0 | +| Low | 9 | + + +# Low Issues + +## L-1: Centralization Risk + +Contracts have owners with privileged rights to perform admin tasks and need to be trusted to not perform malicious updates or drain funds. + +
68 Found Instances + + +- Found in src/modules/AccessControlModuleStandalone.sol [Line: 13](../../../../../home/ryan/Pictures/dev/Rules/src/modules/AccessControlModuleStandalone.sol#L13) + + ```solidity + abstract contract AccessControlModuleStandalone is AccessControlEnumerable { + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 62](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLight.sol#L62) + + ```solidity + function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 67](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLight.sol#L67) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 77](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLight.sol#L77) + + ```solidity + onlyRole(COMPLIANCE_MANAGER_ROLE) + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 50](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L50) + + ```solidity + function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 55](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L55) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 22](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L22) + + ```solidity + Ownable2Step, + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 49](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L49) + + ```solidity + function _onlyComplianceManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 54](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L54) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 21](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L21) + + ```solidity + Ownable2Step, + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 60](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L60) + + ```solidity + function _onlyComplianceManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 65](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L65) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 70](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L70) + + ```solidity + function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 60](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowance.sol#L60) + + ```solidity + function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 65](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowance.sol#L65) + + ```solidity + function _authorizeSetMintAllowance() internal view virtual override onlyRole(ALLOWANCE_OPERATOR_ROLE) {} + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 75](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowance.sol#L75) + + ```solidity + onlyRole(COMPLIANCE_MANAGER_ROLE) + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 19](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L19) + + ```solidity + contract RuleMintAllowanceOwnable2Step is RuleMintAllowanceBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 57](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L57) + + ```solidity + function _onlyComplianceManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 62](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L62) + + ```solidity + function _authorizeSetMintAllowance() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 67](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L67) + + ```solidity + function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 55](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleBlacklist.sol#L55) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 60](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleBlacklist.sol#L60) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 15](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L15) + + ```solidity + contract RuleBlacklistOwnable2Step is RuleBlacklistBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 54](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L54) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 59](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L59) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 76](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980.sol#L76) + + ```solidity + function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 78](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980.sol#L78) + + ```solidity + function _authorizeWhitelistAdd() internal view virtual override onlyRole(WHITELIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 83](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980.sol#L83) + + ```solidity + function _authorizeWhitelistRemove() internal view virtual override onlyRole(WHITELIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 88](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980.sol#L88) + + ```solidity + function _authorizeFrozenlistAdd() internal view virtual override onlyRole(FROZENLIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 93](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980.sol#L93) + + ```solidity + function _authorizeFrozenlistRemove() internal view virtual override onlyRole(FROZENLIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 15](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L15) + + ```solidity + contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 59](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L59) + + ```solidity + function _authorizeMintBurnManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 61](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L61) + + ```solidity + function _authorizeWhitelistAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 66](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L66) + + ```solidity + function _authorizeWhitelistRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 71](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L71) + + ```solidity + function _authorizeFrozenlistAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 76](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L76) + + ```solidity + function _authorizeFrozenlistRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistry.sol [Line: 56](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleIdentityRegistry.sol#L56) + + ```solidity + function _authorizeIdentityRegistryManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 14](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14) + + ```solidity + contract RuleIdentityRegistryOwnable2Step is RuleIdentityRegistryBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 56](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L56) + + ```solidity + function _authorizeIdentityRegistryManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupply.sol [Line: 55](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleMaxTotalSupply.sol#L55) + + ```solidity + function _authorizeMaxTotalSupplyManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 14](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L14) + + ```solidity + contract RuleMaxTotalSupplyOwnable2Step is RuleMaxTotalSupplyBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 57](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L57) + + ```solidity + function _authorizeMaxTotalSupplyManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsList.sol [Line: 58](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSanctionsList.sol#L58) + + ```solidity + function _authorizeSanctionListManager() internal view virtual override onlyRole(SANCTIONLIST_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 17](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L17) + + ```solidity + contract RuleSanctionsListOwnable2Step is RuleSanctionsListBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 60](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L60) + + ```solidity + function _authorizeSanctionListManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 56](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSpenderWhitelist.sol#L56) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 61](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSpenderWhitelist.sol#L61) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 15](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L15) + + ```solidity + contract RuleSpenderWhitelistOwnable2Step is RuleSpenderWhitelistBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 57](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L57) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 62](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L62) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 65](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelist.sol#L65) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 70](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelist.sol#L70) + + ```solidity + function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 75](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelist.sol#L75) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 80](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelist.sol#L80) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 15](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L15) + + ```solidity + contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 58](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L58) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 63](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L63) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 68](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L68) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 73](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L73) + + ```solidity + function _authorizeMintBurnManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 74](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapper.sol#L74) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 79](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapper.sol#L79) + + ```solidity + function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 85](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapper.sol#L85) + + ```solidity + function _onlyRulesManager() internal view virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 90](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapper.sol#L90) + + ```solidity + function _onlyRulesLimitManager() internal view virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 16](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L16) + + ```solidity + contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2Step, Ownable2StepERC165Module { + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 58](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L58) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 63](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L63) + + ```solidity + function _authorizeMintBurnManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 69](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L69) + + ```solidity + function _onlyRulesManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 74](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L74) + + ```solidity + function _onlyRulesLimitManager() internal view virtual override onlyOwner {} + ``` + +
+ + + +## L-2: Unspecific Solidity Pragma + +Consider using a specific version of Solidity in your contracts instead of a wide version. For example, instead of `pragma solidity ^0.8.0;`, use `pragma solidity 0.8.0;` + +
63 Found Instances + + +- Found in src/modules/AccessControlModuleStandalone.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/modules/AccessControlModuleStandalone.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/MetaTxModuleStandalone.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/modules/MetaTxModuleStandalone.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/Ownable2StepERC165Module.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/modules/Ownable2StepERC165Module.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/VersionModule.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/modules/VersionModule.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IAddressList.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/IAddressList.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IERC2980.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/IERC2980.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IERC7943NonFungibleCompliance.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/IERC7943NonFungibleCompliance.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IIdentityRegistry.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/IIdentityRegistry.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/ISanctionsList.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/ISanctionsList.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/ITotalSupply.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/ITotalSupply.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/ITransferContext.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/ITransferContext.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLight.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowance.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleMintAllowanceBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleBlacklistBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleBlacklistBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleERC2980Base.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleSanctionsListBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleSanctionsListBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleWhitelistBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleWhitelistBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/core/RuleNFTAdapter.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/core/RuleNFTAdapter.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/core/RuleTransferValidation.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/core/RuleTransferValidation.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/core/RuleWhitelistShared.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/core/RuleWhitelistShared.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleBlacklist.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistry.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleIdentityRegistry.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupply.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleMaxTotalSupply.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsList.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSanctionsList.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSpenderWhitelist.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelist.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapper.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +
+ + + +## L-3: Address State Variable Set Without Checks + +Check for `address(0)` when assigning values to address state variables. + +
1 Found Instances + + +- Found in src/rules/validation/abstract/base/RuleSanctionsListBase.sol [Line: 125](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleSanctionsListBase.sol#L125) + + ```solidity + sanctionsList = sanctionContractOracle_; + ``` + +
+ + + +## L-4: PUSH0 Opcode + +Solc compiler version 0.8.20 switches the default target EVM version to Shanghai, which means that the generated bytecode will include PUSH0 opcodes. Be sure to select the appropriate EVM version in case you intend to deploy on a chain other than mainnet like L2 chains that may not support PUSH0, otherwise deployment of your contracts will fail. + +
64 Found Instances + + +- Found in src/modules/AccessControlModuleStandalone.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/modules/AccessControlModuleStandalone.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/MetaTxModuleStandalone.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/modules/MetaTxModuleStandalone.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/Ownable2StepERC165Module.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/modules/Ownable2StepERC165Module.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/modules/VersionModule.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/modules/VersionModule.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IAddressList.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/IAddressList.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IERC2980.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/IERC2980.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IERC7943NonFungibleCompliance.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/IERC7943NonFungibleCompliance.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/IIdentityRegistry.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/IIdentityRegistry.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/ISanctionsList.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/ISanctionsList.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/ITotalSupply.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/ITotalSupply.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/ITransferContext.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/ITransferContext.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/interfaces/library/AddressListInterfaceId.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/interfaces/library/AddressListInterfaceId.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLight.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowance.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleMintAllowanceBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleBlacklistBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleBlacklistBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleERC2980Base.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleSanctionsListBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleSanctionsListBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleWhitelistBase.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleWhitelistBase.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/core/RuleNFTAdapter.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/core/RuleNFTAdapter.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/core/RuleTransferValidation.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/core/RuleTransferValidation.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/core/RuleWhitelistShared.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/core/RuleWhitelistShared.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleBlacklist.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistry.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleIdentityRegistry.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupply.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleMaxTotalSupply.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsList.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSanctionsList.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSpenderWhitelist.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelist.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 2](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L2) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapper.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 3](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L3) + + ```solidity + pragma solidity ^0.8.20; + ``` + +
+ + + +## L-5: Modifier Invoked Only Once + +Consider removing the modifier or inlining the logic into the calling function. + +
2 Found Instances + + +- Found in src/rules/validation/abstract/base/RuleWhitelistBase.sol [Line: 80](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleWhitelistBase.sol#L80) + + ```solidity + modifier onlyCheckSpenderManager() { + ``` + +- Found in src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol [Line: 48](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L48) + + ```solidity + modifier onlyCheckSpenderManager() { + ``` + +
+ + + +## L-6: Empty Block + +Consider removing empty blocks. + +
61 Found Instances + + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 62](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLight.sol#L62) + + ```solidity + function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 67](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLight.sol#L67) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLight.sol [Line: 72](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLight.sol#L72) + + ```solidity + function _authorizeComplianceBindingChange(address) + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 50](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L50) + + ```solidity + function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiToken.sol [Line: 55](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightMultiToken.sol#L55) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 49](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L49) + + ```solidity + function _onlyComplianceManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol [Line: 54](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol#L54) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 60](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L60) + + ```solidity + function _onlyComplianceManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 65](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L65) + + ```solidity + function _authorizeTransferApproval() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol [Line: 70](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol#L70) + + ```solidity + function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 60](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowance.sol#L60) + + ```solidity + function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 65](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowance.sol#L65) + + ```solidity + function _authorizeSetMintAllowance() internal view virtual override onlyRole(ALLOWANCE_OPERATOR_ROLE) {} + ``` + +- Found in src/rules/operation/RuleMintAllowance.sol [Line: 70](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowance.sol#L70) + + ```solidity + function _authorizeComplianceBindingChange(address) + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 57](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L57) + + ```solidity + function _onlyComplianceManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 62](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L62) + + ```solidity + function _authorizeSetMintAllowance() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/RuleMintAllowanceOwnable2Step.sol [Line: 67](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/RuleMintAllowanceOwnable2Step.sol#L67) + + ```solidity + function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 63](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleMintAllowanceBase.sol#L63) + + ```solidity + function created(address, uint256) external virtual override onlyBoundToken {} + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 68](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleMintAllowanceBase.sol#L68) + + ```solidity + function destroyed(address, uint256) external virtual override onlyBoundToken {} + ``` + +- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 258](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleMintAllowanceBase.sol#L258) + + ```solidity + function _transferred(address, address, uint256) internal virtual { + ``` + +- Found in src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol [Line: 49](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol#L49) + + ```solidity + function transferred(address, address, uint256) public view override(IERC3643IComplianceContract) {} + ``` + +- Found in src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol [Line: 120](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol#L120) + + ```solidity + function _transferred(address, address, uint256) internal view virtual override { + ``` + +- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 55](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleBlacklist.sol#L55) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklist.sol [Line: 60](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleBlacklist.sol#L60) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 54](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L54) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol [Line: 59](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol#L59) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 76](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980.sol#L76) + + ```solidity + function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 78](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980.sol#L78) + + ```solidity + function _authorizeWhitelistAdd() internal view virtual override onlyRole(WHITELIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 83](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980.sol#L83) + + ```solidity + function _authorizeWhitelistRemove() internal view virtual override onlyRole(WHITELIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 88](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980.sol#L88) + + ```solidity + function _authorizeFrozenlistAdd() internal view virtual override onlyRole(FROZENLIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980.sol [Line: 93](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980.sol#L93) + + ```solidity + function _authorizeFrozenlistRemove() internal view virtual override onlyRole(FROZENLIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 59](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L59) + + ```solidity + function _authorizeMintBurnManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 61](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L61) + + ```solidity + function _authorizeWhitelistAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 66](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L66) + + ```solidity + function _authorizeWhitelistRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 71](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L71) + + ```solidity + function _authorizeFrozenlistAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleERC2980Ownable2Step.sol [Line: 76](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol#L76) + + ```solidity + function _authorizeFrozenlistRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistry.sol [Line: 56](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleIdentityRegistry.sol#L56) + + ```solidity + function _authorizeIdentityRegistryManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol [Line: 56](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L56) + + ```solidity + function _authorizeIdentityRegistryManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupply.sol [Line: 55](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleMaxTotalSupply.sol#L55) + + ```solidity + function _authorizeMaxTotalSupplyManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol [Line: 57](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol#L57) + + ```solidity + function _authorizeMaxTotalSupplyManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsList.sol [Line: 58](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSanctionsList.sol#L58) + + ```solidity + function _authorizeSanctionListManager() internal view virtual override onlyRole(SANCTIONLIST_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol [Line: 60](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol#L60) + + ```solidity + function _authorizeSanctionListManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 56](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSpenderWhitelist.sol#L56) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelist.sol [Line: 61](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSpenderWhitelist.sol#L61) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 57](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L57) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol [Line: 62](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol#L62) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 65](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelist.sol#L65) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 70](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelist.sol#L70) + + ```solidity + function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 75](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelist.sol#L75) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelist.sol [Line: 80](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelist.sol#L80) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 58](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L58) + + ```solidity + function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 63](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L63) + + ```solidity + function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 68](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L68) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol [Line: 73](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol#L73) + + ```solidity + function _authorizeMintBurnManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 74](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapper.sol#L74) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 79](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapper.sol#L79) + + ```solidity + function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 85](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapper.sol#L85) + + ```solidity + function _onlyRulesManager() internal view virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapper.sol [Line: 90](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapper.sol#L90) + + ```solidity + function _onlyRulesLimitManager() internal view virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 58](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L58) + + ```solidity + function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 63](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L63) + + ```solidity + function _authorizeMintBurnManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 69](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L69) + + ```solidity + function _onlyRulesManager() internal view virtual override onlyOwner {} + ``` + +- Found in src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol [Line: 74](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol#L74) + + ```solidity + function _onlyRulesLimitManager() internal view virtual override onlyOwner {} + ``` + +
+ + + +## L-7: Loop Contains `require`/`revert` + +Avoid `require` / `revert` statements in a loop because a single bad item can cause the whole transaction to fail. It's better to forgive on fail and return failed elements post processing of the loop + +
3 Found Instances + + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 42](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L42) + + ```solidity + for (uint256 i = 0; i < addressesToAdd.length; ++i) { + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 48](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L48) + + ```solidity + for (uint256 i = 0; i < addressesToAdd.length; ++i) { + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 109](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L109) + + ```solidity + for (uint256 i = 0; i < addressesToAdd.length; ++i) { + ``` + +
+ + + +## L-8: Costly operations inside loop + +Invoking `SSTORE` operations in loops may waste gas. Use a local variable to hold the loop computation result. + +
7 Found Instances + + +- Found in src/rules/operation/abstract/RuleMintAllowanceBase.sol [Line: 125](../../../../../home/ryan/Pictures/dev/Rules/src/rules/operation/abstract/RuleMintAllowanceBase.sol#L125) + + ```solidity + for (uint256 i = 0; i < minters.length; ++i) { + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 42](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L42) + + ```solidity + for (uint256 i = 0; i < addressesToAdd.length; ++i) { + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 71](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L71) + + ```solidity + for (uint256 i = 0; i < addressesToRemove.length; ++i) { + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 48](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L48) + + ```solidity + for (uint256 i = 0; i < addressesToAdd.length; ++i) { + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 70](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L70) + + ```solidity + for (uint256 i = 0; i < addressesToRemove.length; ++i) { + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 109](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L109) + + ```solidity + for (uint256 i = 0; i < addressesToAdd.length; ++i) { + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 131](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L131) + + ```solidity + for (uint256 i = 0; i < addressesToRemove.length; ++i) { + ``` + +
+ + + +## L-9: Unchecked Return + +Function returns a value but it is ignored. Consider checking the return value. + +
13 Found Instances + + +- Found in src/modules/AccessControlModuleStandalone.sol [Line: 35](../../../../../home/ryan/Pictures/dev/Rules/src/modules/AccessControlModuleStandalone.sol#L35) + + ```solidity + _grantRole(DEFAULT_ADMIN_ROLE, admin); + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol [Line: 62](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol#L62) + + ```solidity + _addAddresses(targetAddresses); + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol [Line: 74](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol#L74) + + ```solidity + _removeAddresses(targetAddresses); + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 85](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L85) + + ```solidity + _listedAddresses.add(targetAddress); + ``` + +- Found in src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol [Line: 93](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L93) + + ```solidity + _listedAddresses.remove(targetAddress); + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 84](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L84) + + ```solidity + _whitelist.add(targetAddress); + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 92](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L92) + + ```solidity + _whitelist.remove(targetAddress); + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 145](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L145) + + ```solidity + _frozenlist.add(targetAddress); + ``` + +- Found in src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol [Line: 153](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L153) + + ```solidity + _frozenlist.remove(targetAddress); + ``` + +- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 110](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleERC2980Base.sol#L110) + + ```solidity + _addWhitelistAddresses(targetAddresses); + ``` + +- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 120](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleERC2980Base.sol#L120) + + ```solidity + _removeWhitelistAddresses(targetAddresses); + ``` + +- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 165](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleERC2980Base.sol#L165) + + ```solidity + _addFrozenlistAddresses(targetAddresses); + ``` + +- Found in src/rules/validation/abstract/base/RuleERC2980Base.sol [Line: 175](../../../../../home/ryan/Pictures/dev/Rules/src/rules/validation/abstract/base/RuleERC2980Base.sol#L175) + + ```solidity + _removeFrozenlistAddresses(targetAddresses); + ``` + +
+ + + diff --git a/doc/security/audits/tools/v0.4.0/claude-audit/CLAUDE_AUDIT.md b/doc/security/audits/tools/v0.4.0/claude-audit/CLAUDE_AUDIT.md new file mode 100644 index 0000000..6c14a0e --- /dev/null +++ b/doc/security/audits/tools/v0.4.0/claude-audit/CLAUDE_AUDIT.md @@ -0,0 +1,405 @@ +# Claude Security Audit — CMTA Rules `v0.4.0` + +| | | +|---|---| +| **Tool** | Claude (Anthropic), driven by a set of custom smart-contract security-audit skills | +| **Codebase audited** | CMTA Rules `v0.4.0` — production contracts under `src/` | +| **Severity framework** | Code4rena (Critical / High / Medium / Low / Info) | +| **Method** | Threat model → privileged-surface enumeration → targeted manual review → executable proofs-of-concept → adversarial self-review of every finding | +| **Date** | 2026-07 | + +**Method note.** The review was driven by a set of internal, task-specific security-audit skills covering the +CMTAT/ERC-3643 compliance model, access control, arithmetic, state and replay analysis, oracle handling, and +EIP-conformance checking. Every candidate finding was then put through an adversarial pass whose explicit job was to +*dismiss or downgrade* it; several were downgraded as a result, and those adjustments are recorded on the findings +below. **The specific internal skills used are intentionally not enumerated here.** + +External tooling used alongside the manual review: **Foundry** (`forge test`, `forge coverage`) and **Slither** +printers (call-graph / inheritance / function-summary) as a comprehension aid only — no static-analysis output was +promoted to a finding without independent manual verification. + +--- + +## 1. Scope + +**In scope:** all production contracts under `src/` — 11 rules (each in an `AccessControl` and an `Ownable2Step` +variant), the shared abstract bases, and the reusable modules. + +**Out of scope:** `lib/` (CMTAT, RuleEngine, OpenZeppelin) and `src/mocks/`. Dependencies are treated as trusted, +but their call contracts are load-bearing and are cited wherever they determine in-scope behaviour — in particular +`CMTAT._mintOverride`, which passes the **minter as `spender`** on every mint, and `RuleEngineBase.transferred`, +which makes the *engine* (not the token) the `msg.sender` inside a rule. Several findings hinge on exactly these two +facts. + +--- + +## 2. Executive summary + +**No Critical or High severity issue was found.** + +| Severity | Count | +|---|---| +| Critical | 0 | +| High | 0 | +| Medium | 0 | +| **Low** | **2** | +| **Info** | **8** | +| Verified-safe / by-design dispositions | 8 | + +The two Low findings are both cases of a rule quietly behaving differently from its siblings, or from its own +documentation — not exploitable by an untrusted party, but capable of causing a real operational failure. + +### Hypotheses that were checked and found NOT to hold + +Two threats were specifically probed **because they would have been High**. Neither exists. They are stated +explicitly, because an unmentioned risk reads as one that was never considered: + +| Hypothesis | Would have been | Result | +|---|---|---| +| An ERC-2771 trusted forwarder can impersonate a bound token and forge/consume approvals | **High** | ❌ **Does not exist.** The three rules that use `_msgSender()` as a *binding identity* (`RuleConditionalTransferLight`, `…MultiToken`, `RuleMintAllowance`) deliberately do **not** inherit `ERC2771Context`. The six rules that *do* inherit it never treat `_msgSender()` as a token or participant identity — `from`/`to`/`spender` are always explicit parameters. The two contract families are disjoint. | +| The hand-rolled keccak assembly in `_transferHash` collides, letting one approval be spent on a different transfer | **High** | ❌ **Does not exist.** Each address is left-aligned into its own 32-byte word (`shl(96, addr)`) and `value` occupies a full word, so the preimage is injective. The `memory-safe` annotation is valid. Confirmed by a 256-run fuzz test over distinct tuples. | + +--- + +## 3. Findings + +### 3.1 Summary + +| ID | Finding | Severity | Status | Component | PoC | +|---|---|---|---|---|---| +| **F-1** | `RuleIdentityRegistry` over-screens vs ERC-3643 (screens sender + spender + minter; spec mandates **receiver only**) | Low | ✅ **Fixed** | `RuleIdentityRegistryBase.sol` | `test_IR1_MintSucceedsWhenMinterNotVerified`, `test_IR1_DelistedHolderCanStillExit` | +| **F-4** | `…MultiToken` approvals keyed by `msg.sender`, not `token` — no per-token isolation behind a RuleEngine | Low | ⚠️ **Documented** (behaviour unchanged) | `RuleConditionalTransferLightMultiTokenBase.sol` | `test_CTL2_MultiTokenApprovalKeyMismatchLeavesStaleApproval_CurrentBehaviour` | +| F-2 | Supply-cap restriction views panic on overflow instead of returning code `50` | Info | ✅ **Fixed** | `RuleMaxTotalSupplyBase.sol` | `test_MTS1_DetectTransferRestrictionReturnsCodeOnOverflow` | +| F-3 | `approveAndTransferIfAllowed` inoperable behind a RuleEngine | Info | ✅ **Fixed** | `RuleConditionalTransferLightBase.sol` | `test_CTL1_ApproveAndTransferIfAllowedWorksOnceEngineIsBound` | +| F-5 | Wrapper does not ERC-165-check its child rules | Info | ⚙️ **Partially fixed** (prerequisite shipped) | `RuleWhitelistWrapperBase.sol` | `test_WW2_NonAddressListChildRuleBricksWrapper_CurrentBehaviour` | +| F-7 | `RuleMintAllowance` eligibility views hardcoded to "allowed" | Info | ⚠️ **Documented** (by design) | `RuleMintAllowanceBase.sol` | `test_MA1_HardcodedEligibilityViewsDisagreeWithEnforcement_CurrentBehaviour` | +| F-8 | `…MultiToken.detectTransferRestriction` depends on `msg.sender` | Info | ✅ **Fixed** | `RuleConditionalTransferLightMultiTokenBase.sol` | `test_CTL2_DetectTransferRestrictionIsMsgSenderDependent_CurrentBehaviour` | +| F-9 | `unbindToken` leaves stale approvals / mint quota | Info | ✅ **Mitigated** | `RuleMintAllowanceBase.sol` | `test_BIND1_MintAllowanceSurvivesRebind_CurrentBehaviour` | +| F-10 | Multi-token documentation contradicted itself on approval scoping | Info | ✅ **Fixed** (doc) | `doc/technical/…MultiToken.md` | ✗ | +| F-14 | Project guide stale (version string, missing rules, missing code `70`) | Info | ✅ **Fixed** (doc) | `CLAUDE.md` / `AGENTS.md` | ✗ | +| F-6 | Wrapper cross-rule OR (`from` in child A, `to` in child B) | — | **NOT-A-FINDING** — documented design (OR semantics) | `RuleWhitelistWrapperBase.sol` | `test_WW1_CrossRuleOrAllowsTransferNoSingleChildAllows` | +| F-11 | ERC-2771 forwarder cannot impersonate a bound token | — | **NOT-A-FINDING** — verified safe | `src/rules/operation/*` | ✗ | +| F-12 | `_transferHash` assembly is injective and memory-safe | — | **NOT-A-FINDING** — verified safe | `…ApprovalBase.sol` | `testFuzz_HASH1_ApprovalBucketsAreDistinct` | +| F-13 | Sanctions screening fails open when the oracle is unset | — | **NOT-A-FINDING** — by design (mirrors `clearIdentityRegistry`) | `RuleSanctionsListBase.sol` | ✗ | +| F-15 | Blacklist screens the minter (no mint carve-out) | — | **NOT-A-FINDING** — correct for a deny-list | `RuleBlacklistBase.sol` | ✗ | +| F-16 | ERC-2980: only `to` must be whitelisted; frozen `from` cannot burn | — | **NOT-A-FINDING** — spec-conformant | `RuleERC2980Base.sol` | ✗ | +| F-17 | `RuleNFTAdapter.transferred(ctx)` unguarded on validation rules | — | **NOT-A-FINDING** — view-only, mutates nothing | `RuleNFTAdapter.sol` | `test_AC5_ContextEntrypointsAreUnguardedButViewOnly` | +| F-18 | CEI inversion in `approveAndTransferIfAllowed` | — | **NOT-A-FINDING** — no exploitable reentrancy | `RuleConditionalTransferLightBase.sol` | ✗ | + +**Tally: 0 Critical, 0 High, 0 Medium, 2 Low, 8 Info; 8 dispositions verified safe or by-design.** + +--- + +### 3.2 F-1 — `RuleIdentityRegistry` over-screens relative to ERC-3643 (Low) ✅ Fixed + +**Severity:** Low *(adversarial pass downgraded this from an initially-proposed Medium: the behaviour is +fail-closed — no unverified party gains any capability — which caps the severity. Accepted.)* + +ERC-3643 mandates exactly **one** identity check: + +> *"The **receiver** MUST be whitelisted on the Identity Registry and verified"* … *"`transferFrom` **works the +> same way**"* … *"`mint` and `forcedTransfer` **only require the receiver** to be whitelisted and verified"* … +> *"The `burn` function **bypasses all checks** on eligibility."* + +The rule screened **three** parties the specification does not require: the **sender**, the **spender**, and — via +the CMTAT v3.3 convention that passes the minter as `spender` — the **minter**. + +**Impact.** + +1. **Issuance halts silently.** An issuer who registers all investors but not their own operational minter EOA finds + *every mint reverting* with `CODE_ADDRESS_SPENDER_NOT_VERIFIED` (57) — a code naming a "spender" on a call that + has no spender. Nothing points at the minter. +2. **De-listed holders were trapped — the more damaging deviation, and one not present in the original finding.** + ERC-3643 screens only the receiver *precisely so that* an investor whose identity lapses (expired claim, revoked + identity) can still **exit their position** by sending to a verified counterparty. Screening the sender meant such + a holder could neither receive **nor** send: their tokens were frozen in place with no path out. + +**Resolution.** The rule is now conformant by default — only the receiver is verified; a mint checks only the +receiver; a burn bypasses everything. The stricter checks are preserved behind **explicit opt-in flags** +(`checkSender`, `checkSpender`, both defaulting to `false`); mint and burn stay exempt from the spender check even +when `checkSpender` is enabled. F-1 is fixed as a *consequence of conformance* rather than as an ad-hoc patch. +**Breaking** — see Remediation. + +--- + +### 3.3 F-4 — Multi-token approvals are keyed by `msg.sender`, not by `token` (Low) ⚠️ Documented + +The rule's entire premise is per-token approval scoping. Approvals are **written** under a caller-supplied `token` +(`_approveTransfer` → `_transferHash(token, …)`) but **consumed** under `msg.sender` +(`transferred` → `_transferred(_msgSender(), …)`). The two keys agree only when the caller *is* the token. + +Behind a `RuleEngine` — the topology the README presents as the default — `msg.sender` is the **engine**, and an +exhaustive case analysis shows **no wiring in which the rule delivers its purpose**: + +| Wiring | Outcome | +|---|---| +| Nothing bound | Every transfer reverts (engine unauthorized) | +| Bind the **token** (the natural reading of "MultiToken") | Every transfer reverts — the token never calls the rule; the engine does | +| Bind the engine, approve with the **token** address | Cannot even record the approval | +| Bind **both**, approve with the token address | Approval written under `H(token,…)`, consumed under `H(engine,…)` → reverts, and the approval is **stranded in storage permanently** | +| Bind the engine, approve with the **engine** address | The only configuration that runs — and approvals become **shared across every token behind that engine**, i.e. exactly the cross-token reuse the rule exists to prevent | + +**Impact.** In the only functioning configuration, an approval recorded for `(alice → bob, 100)` intending token A is +equally consumable on token B. Requires the multi-tenant topology, colliding `(from, to, value)` tuples, and a +trusted `OPERATOR_ROLE` — hence Low, not Medium. + +**Resolution: documented, behaviour unchanged.** The rule is now declared **direct-binding-only**; the technical doc +carries the full case table and an explicit *"Do not add this rule to a `RuleEngine`"*. Supporting true per-token +scoping behind an engine would require the token address to be threaded into `IRuleEngine.transferred(...)` — an +**upstream RuleEngine interface change**, not fixable in this repository. An optional `bindToken` guard that would +make the constraint *enforced* rather than documented remains open (see Potential Improvements). + +*Note:* F-10 (the technical doc simultaneously claiming "Approvals of one token cannot be spent by another" and +admitting the opposite nine lines later) was the most likely cause of real harm here, and is fixed. + +--- + +### 3.4 Info findings (condensed) + +| ID | Finding | Impact | Resolution | +|---|---|---|---| +| **F-2** | `currentSupply + value` overflows in `RuleMaxTotalSupply`'s restriction views, producing `Panic(0x11)` instead of code `50`. These are ERC-1404/ERC-3643 views that must **never** revert. | View-purity/conformance defect. Fail-closed either way (enforcement reverts on the same input), so no bypass. *Adversarial pass downgraded Low → Info: only reachable at `value ≈ 2²⁵⁶`, which cannot correspond to a real mint.* | ✅ Fixed — overflow-safe headroom comparison | +| **F-3** | `approveAndTransferIfAllowed` was structurally impossible behind a RuleEngine: `bindToken` had **one slot serving two roles** — the ERC-20 target (`getTokenBound()`) *and* the authorized caller (`isTokenBound(msg.sender)`). In direct mode they coincide; behind an engine they diverge, and both choices fail. | Binding the engine broke the helper (an engine is not an ERC-20); binding the token left the engine unauthorized and **reverted every transfer and mint**. | ✅ Fixed — the two roles decoupled | +| **F-5** | The wrapper calls `IAddressList(child).areAddressesListed(...)` but, unlike `RuleEngineBase._checkRule`, never verifies the child implements `IAddressList`. A valid-but-wrong `IRule` bricks the scan. | Requires the trusted `RULES_MANAGEMENT_ROLE`; fail-closed; breakage is *input-dependent* (the scan short-circuits once every address is resolved, so some pairs keep working). | ⚙️ Prerequisite shipped (ERC-165 advertisement); the guard itself is open | +| **F-7** | `RuleMintAllowance.canTransfer` / `detectTransferRestriction` are hardcoded to "allowed" — the 3-arg signature carries no minter identity — so the standardized view disagrees with enforcement. | An integrator gating on `canTransfer` is misled. | ⚠️ By design; **documented** where integrators look | +| **F-8** | `…MultiToken.detectTransferRestriction` derives the token from `msg.sender`, so an off-chain `eth_call` from a wallet/explorer always reads "not approved" even for an approved transfer. | Fail-closed, but carries no signal for third-party pre-flight. | ✅ Fixed — caller-explicit views added | +| **F-9** | `unbindToken` clears neither `approvalCounts` nor `mintAllowance`; rebinding makes stale state consumable. The conditional-transfer rule warned about this; the mint-allowance rule did not. | Trusted-role only. | ✅ Mitigated — explicit clearing functions + the missing warning | +| **F-10** | Multi-token doc asserted "Approvals of one token cannot be spent by another" nine lines above a Notes section admitting the opposite. | An integrator reading the first sentence deploys into the broken topology. | ✅ Fixed (doc) | +| **F-14** | Project guide stated version `0.2.0` (code returns `0.4.0`), omitted two rule families and restriction code `70`. | Docs only. | ✅ Fixed (doc) | + +--- + +## 4. Invariant Verification + +Every invariant defined in the threat model, with the **test that establishes it**. An invariant asserted without +evidence is an opinion. + +| # | Property | Holds? | Evidence | +|---|---|---|---| +| `INV-1` | `canTransfer* == (detectTransferRestriction* == 0)` | ✅ | Each rule derives one from the other; verified by inspection across all 11 rules | +| `INV-2` | `transferred` reverts **iff** `detectTransferRestriction*` is non-zero | ⚠️ **Fails for `RuleMintAllowance`** — by design | `test_MA1_HardcodedEligibilityViewsDisagreeWithEnforcement_CurrentBehaviour` → **F-7**; the 3-arg view cannot see the minter | +| `INV-3` | Restriction views never revert | ✅ **(after the F-2 fix)** | Previously failed. Now regression-guarded by `testFuzz_MTS1_DetectRestrictionNeverReverts` over the **full `uint256` domain** | +| `INV-4` | Only the bound entity may call the stateful `transferred` / `created` / `destroyed` | ✅ | `test_CTL1_UnboundCallerCannotConsumeApproval`; `onlyBoundToken` / `onlyTransferExecutor` on every stateful hook | +| `INV-5` | `approvalCounts` never underflows; one approval ⇒ one transfer | ✅ **(stateful)** | `invariant_approvalConservation`, `invariant_noApprovalExceedsTotalRecorded` — **8 192 calls, 0 reverts**, `fail_on_revert = true`. **Mutation-verified**: removing the decrement makes it fail | +| `INV-6` | `_transferHash` is injective | ✅ | `testFuzz_HASH1_ApprovalBucketsAreDistinct` (256 runs) → **F-12** | +| `INV-7` | `mintAllowance` non-increasing across mints, never underflows, `Σ minted ≤ Σ granted` | ✅ **(stateful)** | `invariant_allowanceMatchesGhost` (independent ghost mirror), `invariant_mintedNeverExceedsCredited` — **8 192 calls, 0 reverts**. **Mutation-verified**: an off-by-one in the quota deduction makes it fail | +| `INV-8` | Batch ops never revert on duplicates; single-item ops always do | ✅ | `RuleWhitelistAdd.t.sol`, `RuleWhitelistRemove.t.sol`, `RuleERC2980.t.sol`. *(The one input a batch does not skip is `address(0)` — it reverts, so the emitted event can never report a member that is not in the set.)* | +| `INV-9` | Every emitted code satisfies `canReturnTransferRestrictionCode` and has a message | ✅ | Per-rule unit tests | +| `INV-10` | `_msgSender()` is never used as a binding identity in an `ERC2771Context` rule | ✅ | **F-11** — the two contract families are disjoint. *Held by static reasoning only; a live regression harness is the top open item* | +| `INV-11` | Every `_authorize*` hook bound to a concrete role in every deployment variant | ✅ | All 20 deployment contracts reviewed; all 40 overrides retain their `onlyRole(...)` / `onlyOwner` modifier | +| `INV-12` | Mint/burn handled explicitly by every rule | ✅ **(after the F-1 fix)** | Previously failed on the spender leg of `RuleIdentityRegistry` | + +**Two invariants (`INV-5`, `INV-7`) are now established by a stateful fuzzing suite rather than by fixed-sequence +unit tests**, and both were **mutation-tested** — a real bug was injected, the suite was confirmed to fail, and the +mutation reverted. That is the difference between "we think this holds" and "we tried hard to break it". + +--- + +## 5. Access-Control Verification + +| Rule | Privileged entrypoint | Expected guard | Verified | +|---|---|---|---| +| all address-set rules | `addAddress(es)` / `removeAddress(es)` | `ADDRESS_LIST_ADD_ROLE` / `ADDRESS_LIST_REMOVE_ROLE` / `onlyOwner` | ✅ | +| whitelist family | `setCheckSpender`, `setAllowMint`, `setAllowBurn` | `DEFAULT_ADMIN_ROLE` / `onlyOwner` | ✅ | +| `RuleSanctionsList` | `setSanctionListOracle`, `clearSanctionListOracle` | `SANCTIONLIST_ROLE` / `onlyOwner` | ✅ | +| `RuleMaxTotalSupply` | `setMaxTotalSupply`, `setTokenContract` | `DEFAULT_ADMIN_ROLE` / `onlyOwner` | ✅ | +| `RuleIdentityRegistry` | `setIdentityRegistry`, `clearIdentityRegistry`, `setCheckSender`, `setCheckSpender` | `DEFAULT_ADMIN_ROLE` / `onlyOwner` | ✅ | +| `RuleERC2980` | 8 list-management functions + `setAllowMint` / `setAllowBurn` | 4 dedicated roles / `DEFAULT_ADMIN_ROLE` / `onlyOwner` | ✅ | +| `RuleWhitelistWrapper` | `addRule`, `removeRule`, `setRules`, `clearRules`, `setMaxRules` | `RULES_MANAGEMENT_ROLE` / `onlyOwner` | ✅ | +| `RuleConditionalTransferLight*` | `approveTransfer`, `cancelTransferApproval`, `resetApproval`, `approveAndTransferIfAllowed` | `OPERATOR_ROLE` / `onlyOwner` | ✅ | +| `RuleConditionalTransferLight*` | `transferred`, `created`, `destroyed` | **bound entity only** | ✅ `test_CTL1_UnboundCallerCannotConsumeApproval` | +| `RuleMintAllowance` | `setMintAllowance`, `increase…`, `decrease…`, `clearMintAllowances` | `ALLOWANCE_OPERATOR_ROLE` / `onlyOwner` | ✅ | +| `RuleMintAllowance` | `transferred`, `created`, `destroyed` | **bound entity only** (`onlyBoundToken`) | ✅ | +| all operation rules | `bindToken`, `unbindToken`, `bindRuleEngine`, `unbindRuleEngine` | `COMPLIANCE_MANAGER_ROLE` / `onlyOwner` | ✅ | +| `RuleNFTAdapter` | `transferred(ctx)` overloads | *(none, on validation rules)* | ✅ **Acceptable** — the hooks are `view`: an arbitrary caller can run the check and be reverted by it, but cannot mutate state (**F-17**, pinned by `test_AC5_ContextEntrypointsAreUnguardedButViewOnly`) | + +**No unbound authorization hook, no missing guard, and no selector reachable by an unintended role.** + +**Negative results verified:** an arbitrary caller cannot consume an approval, cannot toggle any rule flag, cannot +mint past a quota, and cannot bind or unbind anything. A `RULES_MANAGEMENT_ROLE` holder cannot approve transfers; an +`OPERATOR_ROLE` holder cannot rebind the token. + +**Centralization premise (stated plainly, not a finding).** `AccessControlModuleStandalone.hasRole` grants the +**default admin every role by construction**. Role separation is therefore *advisory* against a compromised admin — +it limits blast radius between honest operators, not against the admin itself. This is inherent to a +regulated-issuer compliance model, in which an issuer must retain ultimate control, and is a deliberate design +choice rather than a defect. It is called out so no reader mistakes the role table for a security boundary it does +not provide. + +Additionally, all 40 access-control hooks are now `internal view virtual` — making *"authorization never mutates +state"* a **compiler-enforced invariant** rather than a convention. One documented exception remains, structurally +blocked by an upstream (`lib/`) declaration. + +--- + +## 6. Remediation — what was implemented + +Verified against `git log`, `CHANGELOG.md` and the test suite — **not** against the recommendation text. A finding +whose recommendation was written is not a finding that was fixed. + +### 6.1 Code fixes + +| Finding | Status | What actually changed in the code | Verified by | +|---|---|---|---| +| **F-1** | ✅ **Fixed** *(breaking)* | `RuleIdentityRegistry` made ERC-3643 conformant: **only the receiver** is verified. The sender/spender/minter checks moved behind opt-in `checkSender` / `checkSpender` flags (constructor params + setters), both defaulting to `false`. Mint/burn stay exempt from the spender check even when opted in. | **3 PoCs inverted into regression tests** + 10 new tests. `test_IR1_DelistedHolderCanStillExit` pins the exit path that was previously blocked. | +| **F-2** | ✅ **Fixed** | Supply-cap views compare against remaining headroom (`value > maxTotalSupply - currentSupply`) instead of doing a checked addition that panics. Enforcement path unchanged. | **PoC inverted**: `test_MTS1_DetectTransferRestrictionReturnsCodeOnOverflow`. The pre-existing fuzz test was widened to the **full `uint256` domain** — it had been bounded to *avoid* the overflow region, i.e. shaped so it could never find the bug. | +| **F-3** | ✅ **Fixed** | `bindToken`'s two conflated roles were **decoupled**: `bindToken(token)` designates the ERC-20 target; new `bindRuleEngine(engine)` authorizes the engine to call `transferred`. Execution accepts **either** (`isTransferExecutor`). The helper now works in both topologies. Backward compatible. | 9 new tests. `test_CTL1_UnsupportedWiringsStillFail` proves the engine is *rejected* until bound, so the new binding is load-bearing, not cosmetic. | +| **F-8** | ✅ **Fixed** | Added caller-explicit `detectTransferRestrictionForToken` / `canTransferForToken`. The standardized ERC-1404 signatures are unchanged (the token cannot be threaded into them without breaking the standard) but both paths are backed by **one internal helper**, so they are structurally incapable of disagreeing. | 7 new tests, incl. `testFuzz_ViewsAgreeForTheBoundToken` — the regression guard that matters if someone later edits one path. | +| **F-9** | ✅ **Mitigated** | Added `resetApproval(...)` (single- and multi-token) and `clearMintAllowances(address[])`, plus the stale-state warning that was missing from `RuleMintAllowance.bindToken`. The reset functions **deliberately do not require a bound token** — cleaning up *after* `unbindToken` is their entire purpose, and a bound-token check would have made the stale state permanently unclearable. | 9 new tests. Default behaviour is unchanged: state still survives unbind unless explicitly cleared. This adds the tool, not a change to the trust model. | +| **F-5** | ⚙️ **Partially fixed** | **Step 1 shipped:** the six address-set rules now advertise `IAddressList` via ERC-165 (`0x5d10e182`). *Note:* `type(IAddressList).interfaceId` **cannot** be used — it silently omits `contains(address)`, inherited from `IIdentityRegistryContains`, so the wrapper's future check would have looked for an ID no conforming contract could match. A pre-computed constant derived from a flattened interface is used instead. **Step 2 (the wrapper's `_checkRule` guard) is open.** | 7 tests, incl. a regression guard proving the naive interface ID is *wrong* and differs from the correct one by exactly the `contains` selector. | + +### 6.2 Conformance fix beyond the findings + +An additional **breaking** conformance defect was found and fixed while remediating F-1 — it was not in the original +finding list, and it violated **two** standards at once: + +| Issue | Fix | +|---|---| +| `RuleWhitelist`, `RuleWhitelistWrapper` and `RuleERC2980` enabled mint/burn by **whitelisting `address(0)`**. That made standardized getters assert falsehoods: `isVerified(address(0))` returned `true` (ERC-3643 defines `isVerified` as *"is this wallet a valid investor holding the required claims"* — the zero address is not a wallet), and `RuleERC2980.whitelist(address(0))` returned `true` — a **mandatory** ERC-2980 getter. It also meant `removeAddress(address(0))` **silently disabled all minting and burning**. | Mint/burn permission is now an **explicit `allowMint` / `allowBurn` flag** (set together by the `allowMintBurn` constructor parameter, independently settable at runtime so issuance can still be permanently closed while redemptions stay open). The zero address can **never** enter any list. New dedicated restriction codes (`24`/`25`, `64`/`65`) — a blocked mint now says *"minting is not allowed"* instead of the misleading *"sender is not whitelisted"*. Verified: with mint/burn **enabled**, `isVerified(0)`, `contains(0)`, `whitelist(0)` and `frozenlist(0)` are **all `false`**. | + +### 6.3 Documentation-only (behaviour unchanged — stated so it does not read as a code fix) + +| Finding | What changed | +|---|---| +| **F-4** | `…MultiToken` declared **direct-binding-only**, with the exhaustive case table showing why no RuleEngine wiring works. **The code is unchanged.** | +| **F-7** | `RuleMintAllowance.canTransfer` documented as **not authoritative**, with an eligibility-views table directing integrators to `canTransferFrom`. Behaviour is intentional and unchanged. | +| **F-10**, **F-14** | Contradictory multi-token doc corrected; project guide refreshed (version, missing rules, missing restriction code). | +| — | New `RULE_SEMANTICS.md`: a per-rule comparison table (who each rule screens on `from`/`to`/spender/mint/burn, unset-oracle behaviour, stateful?, authoritative view). This directly de-risks the recurring theme behind several findings — rules answering the same question with different conventions, previously discoverable only by reading source. | + +### 6.4 Deliberately declined + +| Item | Reason | +|---|---| +| Hard-cap the wrapper's child-rule count below the existing `maxRules = 10` | **Declined: the child-list size is the operator's responsibility.** `maxRules` already bounds it, the cost is now documented, and a lower hard ceiling would remove legitimate configurations to protect a privileged actor from mis-configuring their own deployment. Documented instead: the scan costs ~8.8k gas per child, is **linear** (measured flat to 200 children), and runs on the *transfer* path — so it is a permanent per-holder tax, **not** a liveness risk (a transfer would not fail to fit in a block until ~3 400 children). | +| A runtime `_looksLikeToken` probe on the approval/transfer hot path | **Declined on gas/complexity grounds.** It would add a call to the token on every operation to guard a misconfiguration a trusted operator already controls. (The same check placed *only* in `bindToken` — a one-time admin call, zero hot-path cost — remains an open option; see below.) | + +### 6.5 Breaking changes and deployer migration + +Two breaking changes landed. Both require action before upgrading: + +1. **`RuleIdentityRegistry` constructor** → `(admin, identityRegistry, checkSender, checkSpender)`. Pass + `false, false` for the ERC-3643-conformant default. A deployment relying on the old stricter behaviour must pass + `true, true`. **The default screening loosens** — no restriction codes changed. +2. **Mint/burn flags.** `RuleWhitelist`'s constructor keeps its shape (`allowMintBurn` now sets flags instead of + whitelisting `address(0)`); `RuleWhitelistWrapper` gains an `allowMintBurn` parameter; `RuleERC2980`'s third + parameter is renamed `allowBurn` → `allowMintBurn`. A deployment that enabled mint/burn by whitelisting + `address(0)` must instead pass `allowMintBurn = true`. **Removed capability:** blacklisting `address(0)` as an + undocumented "halt all issuance" kill switch is no longer possible — halt issuance at the token (CMTAT + pause/deactivate) instead. + +**A version bump is required** before release: the package still reports `0.4.0`, while three constructors changed +arity and four restriction codes were added. + +### 6.6 Test and coverage impact + +| | Before | After | +|---|---|---| +| Tests | 407 | **511** | +| Production line coverage | 94.9% | **97.8%** (1 082 / 1 106) | +| Branch coverage | not measured | **97.3%** (220 / 226) | +| Stateful invariant tests | **0** | 4 (mutation-verified) | +| Fuzz tests | 3 | 10 | + +**Every reachable line of production code is now covered.** The 24 uncovered lines are, without exception, abstract +declarations with no body (`_transferred`, `_detectTransferRestriction`, the `_authorize*` hooks), overridden by +every concrete rule — uncoverable by definition, and no test can move the number. + +That claim is stated precisely because an earlier draft of this report asserted it *without* checking. A +line-by-line audit of the LCOV output found **5 genuinely reachable lines that no test executed**, all of them +introduced by the remediation itself: + +- the four `messageForTransferRestriction` returns for the newly-added mint/burn codes (`24`/`25` on the whitelist + family, `64`/`65` on ERC-2980) — a rejected mint would have reported the right *code* while the message lookup + for it was never exercised, which is precisely the opaque failure the dedicated codes existed to remove; +- the wrapper's degenerate `from == 0 && to == 0` branch, added during review specifically to stop + `RuleWhitelistWrapper` drifting from `RuleWhitelistBase` — an anti-drift guard that nothing checked. + +All five are now covered (5 new tests). The lesson is worth recording: a *percentage* moving in the right direction +concealed a real gap, because the denominator grew at the same time. The check that mattered was enumerating the +uncovered lines and reading them, not watching the headline number. + +--- + +## 7. Potential Improvements (open backlog) + +**These are hardening and quality items, not vulnerabilities.** They are kept separate from the findings on purpose: +conflating them would inflate the apparent risk of the codebase. + +| # | Improvement | Addresses | Effort | Breaking? | Priority | +|---|---|---|---|---|---| +| 1 | **ERC-2771 relay harness** — relay a call through a real forwarder and assert `_msgSender()` resolves to the signer; add a guard test proving the operation rules are *not* ERC-2771 contexts | `INV-10` / F-11 | M | No (tests) | **P1** | +| 2 | **Wrapper child-rule ERC-165 guard** — override `_checkRule` to require `IAddressList` | F-5 (step 2) | S | Yes (`addRule` starts reverting for non-conforming children) | **P1** | +| 3 | **Enforce direct-binding-only on `…MultiToken`** — reject binding a non-token in `bindToken` | F-4 | S | Yes | **Decision** | +| 4 | **Hostile-dependency tests** — reverting/misbehaving sanctions oracle and identity registry | `EXT-1`, `EXT-2` | S | No (tests) | P2 | +| 5 | **Wrapper gas snapshot** — commit a benchmark at 1/5/10 children so a scan-cost regression surfaces as a diff | `WW-4` | S | No | P3 | +| 6 | **Upstream: `internal view virtual` for the four `lib/RuleEngine` access-control hooks** | `INV-11` hardening | S | Upstream | **Blocked** | + +**Rationale.** + +1. **The ERC-2771 harness is the highest-value open item.** `INV-10` is the invariant whose violation would have + been the audit's **only High finding**, and it currently holds *by static reasoning alone*. Nothing in CI enforces + the separation: a future refactor that adds `MetaTxModuleStandalone` to an operation rule would silently hand the + forwarder the ability to impersonate the bound token and forge approvals — and **no test would fail**. This item + is not "extra coverage"; it converts a High-severity near-miss from *assumed* to *enforced*. + +2. The wrapper guard's prerequisite (the ERC-165 advertisement) is already shipped, so this is now a small, isolated + change. It must use the pre-computed interface constant, not `type(IAddressList).interfaceId`. + +3. **Design decision for the maintainers, presented neutrally.** Documentation prevents a *careful* integrator from + misdeploying `…MultiToken`; nothing stops a careless one, and the one wiring that "works" silently shares + approvals across every token behind the engine. A `bindToken` guard would make the constraint unrepresentable at + the cost of one `staticcall` per binding and **zero hot-path gas**. The alternative — leaving it documented — is + defensible if the operator set is small and trusted. *Note:* the existing RuleEngine-integration test currently + asserts the working-but-defeating wiring, and would need to become a negative test. + +6. **Blocked upstream.** Four access-control hooks are declared non-`view` in `lib/RuleEngine`, and Solidity checks + mutability against a virtual's *declared* type. The local overrides are all `view`, but the guarantee cannot be + made project-wide until the upstream declarations change. + +--- + +## 8. Scope & duplicate check + +**In scope.** Every finding is anchored in `src/`, or in documentation describing `src/`. No finding depends on a bug +in `lib/CMTAT`, `lib/RuleEngine`, or OpenZeppelin. + +**Duplicates.** None of the findings duplicate an entry in the audit overview, nor a Slither/Aderyn hit. The two +static analysers reported **nothing to fix** for `v0.4.0` — every hit was a false positive or by-design — and this +review found no overlap with them. F-4's *symptom* was already asserted by an existing integration test (so the +behaviour was known to the team); what this review adds is the stranded-approval consequence, the cross-token +consumption proof, and the self-contradictory documentation. + +**Observations considered and dismissed** — recorded so no reader mistakes an omission for an oversight: + +| Observation | Disposition | +|---|---| +| ERC-2771 forwarder can impersonate role holders | Trusted actor — the forwarder is immutable and set at construction | +| Admin implicitly holds every role | By design (regulated-issuer model); documented | +| Sanctions screening fails open when the oracle is unset; `clearSanctionListOracle` is a one-call kill switch | By design; symmetric with `clearIdentityRegistry`; matches the constructor's optional-oracle contract | +| ERC-2980 requires only `to` to be whitelisted; a frozen `from` cannot burn | Spec-conformant ERC-2980 semantics | +| `RuleBlacklist` screens the minter (no mint carve-out) | Correct and intended — a deny-list *must* screen the minter; the mirror image of the whitelist rules | +| CEI inversion in `approveAndTransferIfAllowed` | Deliberate and documented; the rule custodies no value, and reentrancy could at most consume approvals the operator already granted for the same tuple | +| Reverting sanctions oracle / identity registry bricks transfers | Trusted external dependency; a revert bubbles up with no state corruption | +| `RuleMaxTotalSupply.setTokenContract` can repoint the supply oracle | Trusted role; the rule's NatSpec already states `tokenContract` is trusted to report `totalSupply` honestly | +| Unbounded child-rule loop in the wrapper | Bounded by `maxRules` and the scan's early-exit; cost now measured and documented | +| Wrapper cross-rule OR (`from` in child A, `to` in child B) | Documented design — the wrapper's stated semantics are "listed in **any** child" | + +--- + +## 9. Conclusion + +`v0.4.0` contains **no Critical, High, or Medium severity vulnerability**, and the two threats that would have been +High were specifically probed and found not to exist. + +The issues that *were* found share a single theme, and it is worth naming: **rules that answer the same question +with different conventions, or differently from their own documentation.** Not one of them is exploitable by an +untrusted party — but each could cause a real operational failure for an honest issuer (halted issuance, a trapped +investor, a stranded approval, a misleading pre-flight). The remediation therefore prioritised making the rules +*consistent and spec-conformant* over adding defensive checks, and added `RULE_SEMANTICS.md` so the remaining +differences are documented rather than discovered. + +Two standards-conformance defects were fixed in the process — one against ERC-3643 (`isVerified`), one against +ERC-2980 (the mandatory `whitelist` getter) — neither of which was in the original finding list. + +**Reminder:** this project has **not** undergone a formal third-party security audit. This report is an AI-assisted +review with executable proofs, not a substitute for one. diff --git a/doc/security/audits/tools/v0.4.0/slither-report-feedback.md b/doc/security/audits/tools/v0.4.0/slither-report-feedback.md new file mode 100644 index 0000000..5f41305 --- /dev/null +++ b/doc/security/audits/tools/v0.4.0/slither-report-feedback.md @@ -0,0 +1,99 @@ +# Slither Report — Feedback + +Report version: `v0.4.0` +Slither report: [slither-report.md](./slither-report.md) +Tool: Slither 0.11.5 · Scope: production contracts only (**mocks excluded**) · 168 contracts, 101 detectors, 36 results +Feedback date: 2026-07-14 (re-run after the v0.4.0 security remediation) + +**Delta from the 2026-07-08 run: none.** Same tool version, same scope, and the detector tally is identical +(2 High · 6 Medium · 16 Low · 12 Informational, 36 results). The security remediation landed in this release — +ERC-3643 conformance in `RuleIdentityRegistry`, the explicit `allowMint` / `allowBurn` flags, the +`bindToken` / `bindRuleEngine` split, the overflow-safe supply-cap views — introduced **no new Slither finding and +resolved none**, which is expected: every finding below is a false positive or a by-design pattern, so none of them +was ever tracking a real defect that a fix could clear. The triage below therefore stands unchanged. + +Verdicts: + +| Verdict | Meaning | +|---|---| +| **Acknowledged** | Known, accepted by design; no change planned. | +| **By design** | Behaviour is intentional and required by the architecture. | +| **Fixed** | Resolved in the codebase. | +| **To fix** | Will be addressed in a future revision. | +| **False positive** | Tool mis-identification; no real issue exists. | +| **Out of scope** | Reported location is in dependency code outside this repository. | + +--- + +## arbitrary-send-erc20 (High) — 2 instances + +**Verdict: False positive** + +Flagged functions: +- `RuleConditionalTransferLightBase.approveAndTransferIfAllowed(address,address,uint256)` (`src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L86-L101`) +- `RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed(address,address,address,uint256)` (`src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L121-L138`) + +`from` is a parameter, but neither is an arbitrary-drain primitive. In both: + +1. Access is restricted by `onlyTransferApprover`. +2. A transfer approval must have been recorded (`_approveTransfer(...)`) — the multi-token variant additionally requires `isTokenBound(token)`. +3. The contract checks `IERC20(token).allowance(from, address(this)) >= value` before transferring. +4. `SafeERC20.safeTransferFrom` only hardens ERC-20 return handling; it does not change the authorization model. + +The second instance is new in `v0.4.0` — it is the multi-token sibling of the `v0.3.0` finding and shares the identical guard chain. + +--- + +## unused-return (Medium) — 6 instances + +**Verdict: False positive** + +Flagged calls are `EnumerableSet.add/remove` in internal single-item helpers (`RuleAddressSetInternal`, `RuleERC2980Internal`). The public single-item entrypoints already perform presence/absence pre-checks, making the boolean return redundant in the internal helpers. + +--- + +## calls-loop (Low) — 16 instances + +**Verdict: By design** + +All instances stem from `RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(...)`, which must query each child whitelist rule to implement OR aggregation. The external calls in this loop are intrinsic to the wrapper design, and child rules are read-only. + +--- + +## assembly (Informational) — 2 instances + +**Verdict: By design** + +- `RuleConditionalTransferLightApprovalBase._transferHash(...)` (`#L125-L134`) +- `RuleConditionalTransferLightMultiTokenBase._transferHash(...)` (`#L334-L348`) + +Both use a small, memory-safe inline-assembly block to compute the transfer-tuple hash efficiently. Intentional and bounded. The second instance is new in `v0.4.0` (multi-token variant of the same pattern). + +--- + +## naming-convention (Informational) — 2 instances + +**Verdict: By design** + +`_operator` naming in `RuleERC2980Base.whitelist/frozenlist` mirrors the ERC-2980 interface. Keeping spec-aligned argument names is intentional. + +--- + +## unused-state (Informational) — 8 instances + +**Verdict: False positive** + +All instances are `RuleNFTAdapter` selector constants reported as unused in specific concrete contracts. They are part of inherited dispatch logic used across adapter paths; this is a per-contract inheritance-analysis limitation. + +--- + +## Delta from v0.3.0 + +- **arbitrary-send-erc20**: 1 → **2** (new: `RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed`, same guard chain). +- **assembly**: 1 → **2** (new: `RuleConditionalTransferLightMultiTokenBase._transferHash`, same memory-safe pattern). +- **unindexed-event-address** (2, Info, `lib/`): no longer reported — dependency paths excluded via `lib` filter. +- All other detectors and instance counts unchanged; all dispositions carried over. + +## Executive triage + +**Nothing to fix.** No finding is exploitable. The two High-severity `arbitrary-send-erc20` hits are false positives (authorized, approval-gated, allowance-checked compliance flow); everything else is by-design or a tool limitation. diff --git a/doc/security/audits/tools/v0.4.0/slither-report.md b/doc/security/audits/tools/v0.4.0/slither-report.md new file mode 100644 index 0000000..7f088e4 --- /dev/null +++ b/doc/security/audits/tools/v0.4.0/slither-report.md @@ -0,0 +1,351 @@ +# Slither Report — `v0.4.0` + +```bash +slither . --checklist --filter-paths "node_modules,submodules,test,forge-std,mocks,lib" +``` + +Tool: **Slither 0.11.5** · Scope: production contracts only (**mocks excluded**) · 168 contracts, 101 detectors, 36 results +Run date: 2026-07-14 (re-run after the v0.4.0 security remediation) + +**Result: 2 High · 6 Medium · 16 Low · 12 Informational.** + +| Detector | Severity | Instances | Assessment | +|---|---|---|---| +| `arbitrary-send-erc20` | **High** | 2 | **False positive** — `approveAndTransferIfAllowed` is gated by `onlyTransferApprover`, a recorded approval, an allowance check, and a bound token | +| `unused-return` | Medium | 6 | False positive — `EnumerableSet` add/remove returns are guaranteed by outer pre-checks | +| `calls-loop` | Low | 16 | By design — the wrapper's child-rule scan and batch list ops; cost model documented | +| `assembly` | Info | 2 | By design — the reviewed, `memory-safe` `_transferHash` keccak preimage | +| `naming-convention` | Info | 2 | By design — names are fixed by the ERC-2980 / ERC-3643 specs | +| `unused-state` | Info | 8 | False positive — per-contract analysis misses selectors consumed by inherited dispatch | + +**Nothing to fix.** The two High hits are false positives; every other finding is by-design or a tool limitation — triaged individually in the feedback file. + +Full triage: [slither-report-feedback.md](./slither-report-feedback.md) · Overview: [AUDIT_OVERVIEW.md](../../AUDIT_OVERVIEW.md) · Manual audit: [CLAUDE_AUDIT.md](./claude-audit/CLAUDE_AUDIT.md) + +--- + +**THIS CHECKLIST IS NOT COMPLETE**. Use `--show-ignored-findings` to show all the results. +Summary + - [arbitrary-send-erc20](#arbitrary-send-erc20) (2 results) (High) + - [unused-return](#unused-return) (6 results) (Medium) + - [calls-loop](#calls-loop) (16 results) (Low) + - [assembly](#assembly) (2 results) (Informational) + - [naming-convention](#naming-convention) (2 results) (Informational) + - [unused-state](#unused-state) (8 results) (Informational) +## arbitrary-send-erc20 +Impact: High +Confidence: High + - [ ] ID-0 +[RuleConditionalTransferLightMultiTokenBase.approveAndTransferIfAllowed(address,address,address,uint256)](src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L126-L142) uses arbitrary from in transferFrom: [IERC20(token).safeTransferFrom(from,to,value)](src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L140) + +src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L126-L142 + + + - [ ] ID-1 +[RuleConditionalTransferLightBase.approveAndTransferIfAllowed(address,address,uint256)](src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L113-L128) uses arbitrary from in transferFrom: [IERC20(token).safeTransferFrom(from,to,value)](src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L126) + +src/rules/operation/abstract/RuleConditionalTransferLightBase.sol#L113-L128 + + +## unused-return +Impact: Medium +Confidence: Medium + - [ ] ID-2 +[RuleERC2980Internal._removeWhitelistAddress(address)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L91-L93) ignores return value by [_whitelist.remove(targetAddress)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L92) + +src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L91-L93 + + + - [ ] ID-3 +[RuleAddressSetInternal._addAddress(address)](src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L84-L86) ignores return value by [_listedAddresses.add(targetAddress)](src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L85) + +src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L84-L86 + + + - [ ] ID-4 +[RuleERC2980Internal._removeFrozenlistAddress(address)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L152-L154) ignores return value by [_frozenlist.remove(targetAddress)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L153) + +src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L152-L154 + + + - [ ] ID-5 +[RuleAddressSetInternal._removeAddress(address)](src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L92-L94) ignores return value by [_listedAddresses.remove(targetAddress)](src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L93) + +src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol#L92-L94 + + + - [ ] ID-6 +[RuleERC2980Internal._addFrozenlistAddress(address)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L144-L146) ignores return value by [_frozenlist.add(targetAddress)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L145) + +src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L144-L146 + + + - [ ] ID-7 +[RuleERC2980Internal._addWhitelistAddress(address)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L83-L85) ignores return value by [_whitelist.add(targetAddress)](src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L84) + +src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol#L83-L85 + + +## calls-loop +Impact: Low +Confidence: Medium + - [ ] ID-8 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleWhitelistShared.transferred(address,address,address,uint256) + RuleWhitelistShared._transferredFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-9 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleNFTAdapter.transferred(ITransferContext.MultiTokenTransferContext) + RuleWhitelistShared._transferredFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-10 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleNFTAdapter.detectTransferRestriction(address,address,uint256,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-11 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleTransferValidation.detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-12 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleNFTAdapter.canTransferFrom(address,address,address,uint256,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-13 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleWhitelistWrapperHarnessInternal.exposedTransferredSpenderInternal(address,address,address,uint256) + RuleWhitelistWrapperBase._transferred(address,address,address,uint256) + RuleWhitelistShared._transferredFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-14 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleNFTAdapter.transferred(ITransferContext.FungibleTransferContext) + RuleWhitelistShared._transferredFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-15 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleTransferValidation.detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-16 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleTransferValidation.canTransferFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-17 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleNFTAdapter.canTransfer(address,address,uint256,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-18 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleNFTAdapter.transferred(address,address,address,uint256,uint256) + RuleWhitelistShared._transferredFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-19 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleWhitelistWrapperBase.isVerified(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-20 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleTransferValidation.canTransfer(address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-21 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleWhitelistShared.transferred(address,address,uint256) + RuleWhitelistWrapperBase._transferred(address,address,uint256) + RuleWhitelistShared._transferred(address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-22 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleNFTAdapter.transferred(address,address,uint256,uint256) + RuleWhitelistWrapperBase._transferred(address,address,uint256) + RuleWhitelistShared._transferred(address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + + - [ ] ID-23 +[RuleWhitelistWrapperBase._detectTransferRestrictionForTargets(address[])](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291) has external calls inside a loop: [isListed = IAddressList(rule(i)).areAddressesListed(targetAddress)](src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L271) + Calls stack containing the loop: + RuleNFTAdapter.detectTransferRestrictionFrom(address,address,address,uint256,uint256) + RuleWhitelistWrapperBase._detectTransferRestrictionFrom(address,address,address,uint256) + RuleWhitelistWrapperBase._detectTransferRestriction(address,address,uint256) + RuleWhitelistWrapperBase._isListedInAnyChild(address) + +src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol#L260-L291 + + +## assembly +Impact: Informational +Confidence: High + - [ ] ID-24 +[RuleConditionalTransferLightApprovalBase._transferHash(address,address,uint256)](src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L150-L159) uses assembly + - [INLINE ASM](src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L152-L158) + +src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol#L150-L159 + + + - [ ] ID-25 +[RuleConditionalTransferLightMultiTokenBase._transferHash(address,address,address,uint256)](src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L441-L455) uses assembly + - [INLINE ASM](src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L447-L454) + +src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol#L441-L455 + + +## naming-convention +Impact: Informational +Confidence: High + - [ ] ID-26 +Parameter [RuleERC2980Base.frozenlist(address)._operator](src/rules/validation/abstract/base/RuleERC2980Base.sol#L376) is not in mixedCase + +src/rules/validation/abstract/base/RuleERC2980Base.sol#L376 + + + - [ ] ID-27 +Parameter [RuleERC2980Base.whitelist(address)._operator](src/rules/validation/abstract/base/RuleERC2980Base.sol#L327) is not in mixedCase + +src/rules/validation/abstract/base/RuleERC2980Base.sol#L327 + + +## unused-state +Impact: Informational +Confidence: High + - [ ] ID-28 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_RULE_ENGINE](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L27) is never used in [RuleIdentityRegistryOwnable2Step](src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14-L57) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L27 + + + - [ ] ID-29 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC7943](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L31-L32) is never used in [RuleIdentityRegistryOwnable2Step](src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14-L57) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L31-L32 + + + - [ ] ID-30 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC7943_FROM](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L36-L37) is never used in [RuleIdentityRegistryOwnable2Step](src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14-L57) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L36-L37 + + + - [ ] ID-31 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC3643](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L23) is never used in [RuleIdentityRegistryOwnable2Step](src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol#L14-L57) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L23 + + + - [ ] ID-32 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC7943](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L31-L32) is never used in [RuleIdentityRegistry](src/rules/validation/deployment/RuleIdentityRegistry.sol#L14-L57) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L31-L32 + + + - [ ] ID-33 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_RULE_ENGINE](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L27) is never used in [RuleIdentityRegistry](src/rules/validation/deployment/RuleIdentityRegistry.sol#L14-L57) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L27 + + + - [ ] ID-34 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC7943_FROM](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L36-L37) is never used in [RuleIdentityRegistry](src/rules/validation/deployment/RuleIdentityRegistry.sol#L14-L57) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L36-L37 + + + - [ ] ID-35 +[RuleNFTAdapter.TRANSFERRED_SELECTOR_ERC3643](src/rules/validation/abstract/core/RuleNFTAdapter.sol#L23) is never used in [RuleIdentityRegistry](src/rules/validation/deployment/RuleIdentityRegistry.sol#L14-L57) + +src/rules/validation/abstract/core/RuleNFTAdapter.sol#L23 + + diff --git a/doc/specification/RulesSpecificationv0.3.0.pdf b/doc/specification/RulesSpecificationv0.3.0.pdf new file mode 100644 index 0000000..53754ab Binary files /dev/null and b/doc/specification/RulesSpecificationv0.3.0.pdf differ diff --git a/doc/specification/RulesSpecificationv0.2.0.pdf b/doc/specification/archive/RulesSpecificationv0.2.0.pdf similarity index 100% rename from doc/specification/RulesSpecificationv0.2.0.pdf rename to doc/specification/archive/RulesSpecificationv0.2.0.pdf diff --git a/doc/specification/cover_page.odg b/doc/specification/cover_page.odg index 855eff6..4645f13 100644 Binary files a/doc/specification/cover_page.odg and b/doc/specification/cover_page.odg differ diff --git a/doc/specification/cover_page.pdf b/doc/specification/cover_page.pdf index 0b3519c..8dd7fe2 100644 Binary files a/doc/specification/cover_page.pdf and b/doc/specification/cover_page.pdf differ diff --git a/doc/surya/surya_graph/surya_graph_MockERC20WithTransferContext.sol.png b/doc/surya/surya_graph/surya_graph_MockERC20WithTransferContext.sol.png index e69de29..70b2150 100644 Binary files a/doc/surya/surya_graph/surya_graph_MockERC20WithTransferContext.sol.png and b/doc/surya/surya_graph/surya_graph_MockERC20WithTransferContext.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_MockERC721WithTransferContext.sol.png b/doc/surya/surya_graph/surya_graph_MockERC721WithTransferContext.sol.png index e69de29..b4e620e 100644 Binary files a/doc/surya/surya_graph/surya_graph_MockERC721WithTransferContext.sol.png and b/doc/surya/surya_graph/surya_graph_MockERC721WithTransferContext.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_Ownable2StepERC165Module.sol.png b/doc/surya/surya_graph/surya_graph_Ownable2StepERC165Module.sol.png new file mode 100644 index 0000000..aea364a Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_Ownable2StepERC165Module.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleBlacklist.sol.png b/doc/surya/surya_graph/surya_graph_RuleBlacklist.sol.png index e69de29..644c975 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleBlacklist.sol.png and b/doc/surya/surya_graph/surya_graph_RuleBlacklist.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleBlacklistOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleBlacklistOwnable2Step.sol.png index e69de29..36e969b 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleBlacklistOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleBlacklistOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png index 184ce94..66c1899 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiToken.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiToken.sol.png new file mode 100644 index 0000000..4ef47d6 Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiToken.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenBase.sol.png new file mode 100644 index 0000000..7e79f6d Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenBase.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.png new file mode 100644 index 0000000..5b4fa6b Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png new file mode 100644 index 0000000..836581b Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightOwnable2Step.sol.png index 1138317..8aac685 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleConditionalTransferLightOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleERC2980.sol.png b/doc/surya/surya_graph/surya_graph_RuleERC2980.sol.png index e69de29..a620ceb 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleERC2980.sol.png and b/doc/surya/surya_graph/surya_graph_RuleERC2980.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleERC2980Ownable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleERC2980Ownable2Step.sol.png index e69de29..69fec14 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleERC2980Ownable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleERC2980Ownable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleIdentityRegistryOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleIdentityRegistryOwnable2Step.sol.png index 2740710..c5964a6 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleIdentityRegistryOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleIdentityRegistryOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyOwnable2Step.sol.png index 478dd4c..50a285b 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleMaxTotalSupplyOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleMintAllowance.sol.png b/doc/surya/surya_graph/surya_graph_RuleMintAllowance.sol.png new file mode 100644 index 0000000..b0c8706 Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleMintAllowance.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleMintAllowanceBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceBase.sol.png new file mode 100644 index 0000000..90df11f Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceBase.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleMintAllowanceInvariantStorage.sol.png b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceInvariantStorage.sol.png new file mode 100644 index 0000000..5b4fa6b Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceInvariantStorage.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleMintAllowanceOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceOwnable2Step.sol.png new file mode 100644 index 0000000..8ed397f Binary files /dev/null and b/doc/surya/surya_graph/surya_graph_RuleMintAllowanceOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleSanctionsListOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleSanctionsListOwnable2Step.sol.png index efe30a2..f3f52a4 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleSanctionsListOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleSanctionsListOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelist.sol.png b/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelist.sol.png index e69de29..4eadee9 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelist.sol.png and b/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelist.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistBase.sol.png index 2095cd9..2db56c3 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistBase.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistOwnable2Step.sol.png index e69de29..d687e14 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleSpenderWhitelistOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelist.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelist.sol.png index e69de29..86a8825 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelist.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelist.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelistBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelistBase.sol.png index 49ddfce..0d7ea6b 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelistBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelistBase.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelistOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelistOwnable2Step.sol.png index e69de29..87c918b 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelistOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelistOwnable2Step.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapper.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapper.sol.png index 071894a..d521009 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapper.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapper.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperBase.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperBase.sol.png index 07a074e..3523933 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperBase.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperBase.sol.png differ diff --git a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperOwnable2Step.sol.png b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperOwnable2Step.sol.png index 9d1c456..719eecd 100644 Binary files a/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperOwnable2Step.sol.png and b/doc/surya/surya_graph/surya_graph_RuleWhitelistWrapperOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_Ownable2StepERC165Module.sol.png b/doc/surya/surya_inheritance/surya_inheritance_Ownable2StepERC165Module.sol.png new file mode 100644 index 0000000..bde6d04 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_Ownable2StepERC165Module.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleBlacklistOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleBlacklistOwnable2Step.sol.png index 131b819..8a9378e 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleBlacklistOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleBlacklistOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png index d56372a..dc9f809 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiToken.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiToken.sol.png new file mode 100644 index 0000000..e05b601 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiToken.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenBase.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenBase.sol.png new file mode 100644 index 0000000..b1e4857 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenBase.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.png new file mode 100644 index 0000000..afe3a62 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png new file mode 100644 index 0000000..34cb43a Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightOwnable2Step.sol.png index 219317e..9eb4c54 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleERC2980Ownable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleERC2980Ownable2Step.sol.png index 2cec104..bbf3408 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleERC2980Ownable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleERC2980Ownable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleIdentityRegistryOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleIdentityRegistryOwnable2Step.sol.png index 68e2d20..4d69bb6 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleIdentityRegistryOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleIdentityRegistryOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyOwnable2Step.sol.png index 7e9a046..5868131 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupplyOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowance.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowance.sol.png new file mode 100644 index 0000000..3c6c42c Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowance.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceBase.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceBase.sol.png new file mode 100644 index 0000000..3b91e2b Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceBase.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceInvariantStorage.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceInvariantStorage.sol.png new file mode 100644 index 0000000..bf91969 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceInvariantStorage.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceOwnable2Step.sol.png new file mode 100644 index 0000000..2213385 Binary files /dev/null and b/doc/surya/surya_inheritance/surya_inheritance_RuleMintAllowanceOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleSanctionsListOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleSanctionsListOwnable2Step.sol.png index 6cc595f..cf44c9e 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleSanctionsListOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleSanctionsListOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleSpenderWhitelistOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleSpenderWhitelistOwnable2Step.sol.png index 7e43526..4f92c3f 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleSpenderWhitelistOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleSpenderWhitelistOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistOwnable2Step.sol.png index 003c369..d3fc429 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistOwnable2Step.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapper.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapper.sol.png index c1f68fc..98f6cc6 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapper.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapper.sol.png differ diff --git a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapperOwnable2Step.sol.png b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapperOwnable2Step.sol.png index c450eba..18dd771 100644 Binary files a/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapperOwnable2Step.sol.png and b/doc/surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapperOwnable2Step.sol.png differ diff --git a/doc/surya/surya_report/surya_report_Ownable2StepERC165Module.sol.md b/doc/surya/surya_report/surya_report_Ownable2StepERC165Module.sol.md new file mode 100644 index 0000000..2383145 --- /dev/null +++ b/doc/surya/surya_report/surya_report_Ownable2StepERC165Module.sol.md @@ -0,0 +1,27 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./modules/Ownable2StepERC165Module.sol | 50ac2979efcf101b8db4fbc27720e9e92fedd47a | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **Ownable2StepERC165Module** | Implementation | ERC165 ||| +| └ | supportsInterface | Public ❗️ | |NO❗️ | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleBlacklistOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleBlacklistOwnable2Step.sol.md index 3b4d15f..8fadfa0 100644 --- a/doc/surya/surya_report/surya_report_RuleBlacklistOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleBlacklistOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleBlacklistOwnable2Step.sol | 7124f42b2a3b6352300ffdaba32b8f6ce31862bd | +| ./rules/validation/deployment/RuleBlacklistOwnable2Step.sol | d1d803fa9f1f3db0a21ef8cde1b9a96e910cd728 | ### Contracts Description Table @@ -15,10 +15,11 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleBlacklistOwnable2Step** | Implementation | RuleBlacklistBase, Ownable2Step ||| +| **RuleBlacklistOwnable2Step** | Implementation | RuleBlacklistBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleBlacklistBase Ownable | | └ | _authorizeAddressListAdd | Internal 🔒 | | onlyOwner | | └ | _authorizeAddressListRemove | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md index bc4c64c..d105630 100644 --- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLight.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/operation/RuleConditionalTransferLight.sol | 0296e0b6f9efba3f65c654982ee5e8b8b0d6b52c | +| ./rules/operation/RuleConditionalTransferLight.sol | 62dd5be103c29fa0ad3ec6ee371468a8e0edc01f | ### Contracts Description Table @@ -15,11 +15,12 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleConditionalTransferLight** | Implementation | AccessControlModuleStandalone, RuleConditionalTransferLightBase ||| +| **RuleConditionalTransferLight** | Implementation | AccessControlModuleStandalone, RuleConditionalTransferLightBase, ERC3643ComplianceRolesStorage ||| | └ | | Public ❗️ | 🛑 | AccessControlModuleStandalone | | └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _authorizeTransferApproval | Internal 🔒 | | onlyRole | | └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyRole | +| └ | _authorizeComplianceBindingChange | Internal 🔒 | | onlyRole | ### Legend diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightBase.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightBase.sol.md index 2a94c95..2f3938f 100644 --- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightBase.sol.md +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightBase.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/operation/abstract/RuleConditionalTransferLightBase.sol | 32e9a0425e0706764501785c3a4220b65ddeaec7 | +| ./rules/operation/abstract/RuleConditionalTransferLightBase.sol | ca4e40de3d7c746518faf45ef82d6c21c846e650 | ### Contracts Description Table diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiToken.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiToken.sol.md new file mode 100644 index 0000000..2d1dbe6 --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiToken.sol.md @@ -0,0 +1,30 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/RuleConditionalTransferLightMultiToken.sol | 5a26c4179ebee4b52842ca0073933366fcb4ce6c | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleConditionalTransferLightMultiToken** | Implementation | AccessControlModuleStandalone, RuleConditionalTransferLightMultiTokenBase, ERC3643ComplianceRolesStorage ||| +| └ | | Public ❗️ | 🛑 | AccessControlModuleStandalone | +| └ | supportsInterface | Public ❗️ | |NO❗️ | +| └ | _authorizeTransferApproval | Internal 🔒 | | onlyRole | +| └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyRole | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenBase.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenBase.sol.md new file mode 100644 index 0000000..a163edc --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenBase.sol.md @@ -0,0 +1,48 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol | 005bde36fdb674f7b60284457c9206700fe85387 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleConditionalTransferLightMultiTokenBase** | Implementation | VersionModule, ERC3643ComplianceModule, RuleConditionalTransferLightMultiTokenInvariantStorage, IRule ||| +| └ | _authorizeTransferApproval | Internal 🔒 | | | +| └ | canReturnTransferRestrictionCode | External ❗️ | |NO❗️ | +| └ | messageForTransferRestriction | External ❗️ | |NO❗️ | +| └ | created | External ❗️ | 🛑 | onlyBoundToken | +| └ | destroyed | External ❗️ | 🛑 | onlyBoundToken | +| └ | approveTransfer | Public ❗️ | 🛑 | onlyTransferApprover | +| └ | cancelTransferApproval | Public ❗️ | 🛑 | onlyTransferApprover | +| └ | approvedCount | Public ❗️ | |NO❗️ | +| └ | approveAndTransferIfAllowed | Public ❗️ | 🛑 | onlyTransferApprover | +| └ | transferred | Public ❗️ | 🛑 | onlyTransferExecutor | +| └ | transferred | Public ❗️ | 🛑 | onlyTransferExecutor | +| └ | transferred | External ❗️ | 🛑 | onlyTransferExecutor | +| └ | detectTransferRestriction | Public ❗️ | |NO❗️ | +| └ | detectTransferRestrictionFrom | Public ❗️ | |NO❗️ | +| └ | canTransfer | Public ❗️ | |NO❗️ | +| └ | canTransferFrom | Public ❗️ | |NO❗️ | +| └ | _authorizeTransferExecution | Internal 🔒 | | | +| └ | _authorizeComplianceBindingChange | Internal 🔒 | 🛑 | | +| └ | _approveTransfer | Internal 🔒 | 🛑 | | +| └ | _cancelTransferApproval | Internal 🔒 | 🛑 | | +| └ | _transferred | Internal 🔒 | 🛑 | | +| └ | _transferHash | Internal 🔒 | | | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.md new file mode 100644 index 0000000..997b9a4 --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenInvariantStorage.sol.md @@ -0,0 +1,26 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol | 8376ee8d2ce771fdb57e13ef3832258220f89496 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleConditionalTransferLightMultiTokenInvariantStorage** | Implementation | RuleSharedInvariantStorage ||| + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.md new file mode 100644 index 0000000..20808f5 --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightMultiTokenOwnable2Step.sol.md @@ -0,0 +1,30 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol | 3da3c69fd927f5e21b6881156369ff3c7957af38 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleConditionalTransferLightMultiTokenOwnable2Step** | Implementation | RuleConditionalTransferLightMultiTokenBase, Ownable2Step, Ownable2StepERC165Module ||| +| └ | | Public ❗️ | 🛑 | Ownable | +| └ | supportsInterface | Public ❗️ | |NO❗️ | +| └ | _authorizeTransferApproval | Internal 🔒 | | onlyOwner | +| └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyOwner | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightOwnable2Step.sol.md index fdf137b..440233a 100644 --- a/doc/surya/surya_report/surya_report_RuleConditionalTransferLightOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleConditionalTransferLightOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/operation/RuleConditionalTransferLightOwnable2Step.sol | 52c514a3996546640354595fd890941d0f8875ac | +| ./rules/operation/RuleConditionalTransferLightOwnable2Step.sol | e94c0dc5da33a08b1438eed005fffaf59d213def | ### Contracts Description Table @@ -15,11 +15,12 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleConditionalTransferLightOwnable2Step** | Implementation | RuleConditionalTransferLightBase, Ownable2Step ||| +| **RuleConditionalTransferLightOwnable2Step** | Implementation | RuleConditionalTransferLightBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | Ownable | | └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _authorizeTransferApproval | Internal 🔒 | | onlyOwner | | └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyOwner | +| └ | _authorizeComplianceBindingChange | Internal 🔒 | | onlyOwner | ### Legend diff --git a/doc/surya/surya_report/surya_report_RuleERC2980Ownable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleERC2980Ownable2Step.sol.md index 8a62e10..5ea80fa 100644 --- a/doc/surya/surya_report/surya_report_RuleERC2980Ownable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleERC2980Ownable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleERC2980Ownable2Step.sol | 65d1a4469f2d80fda332499a277f75cfd96f07b3 | +| ./rules/validation/deployment/RuleERC2980Ownable2Step.sol | 059ea6668baf03d1ac9f3c7f3c85e2b09c01306f | ### Contracts Description Table @@ -15,12 +15,13 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleERC2980Ownable2Step** | Implementation | RuleERC2980Base, Ownable2Step ||| +| **RuleERC2980Ownable2Step** | Implementation | RuleERC2980Base, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleERC2980Base Ownable | | └ | _authorizeWhitelistAdd | Internal 🔒 | | onlyOwner | | └ | _authorizeWhitelistRemove | Internal 🔒 | | onlyOwner | | └ | _authorizeFrozenlistAdd | Internal 🔒 | | onlyOwner | | └ | _authorizeFrozenlistRemove | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_RuleIdentityRegistryOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleIdentityRegistryOwnable2Step.sol.md index 5c2f7a8..49a8878 100644 --- a/doc/surya/surya_report/surya_report_RuleIdentityRegistryOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleIdentityRegistryOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol | f498c93b91bcdd36aa42817847953f3a308d51a6 | +| ./rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol | 99b6856a407f4a428a54b05ff817868f1d74e909 | ### Contracts Description Table @@ -15,9 +15,10 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleIdentityRegistryOwnable2Step** | Implementation | RuleIdentityRegistryBase, Ownable2Step ||| +| **RuleIdentityRegistryOwnable2Step** | Implementation | RuleIdentityRegistryBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleIdentityRegistryBase Ownable | | └ | _authorizeIdentityRegistryManager | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | ### Legend diff --git a/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyOwnable2Step.sol.md index 2d964cc..4445776 100644 --- a/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleMaxTotalSupplyOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol | 6f8fc83269973f6497ca27721ae7b893cd6104e0 | +| ./rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol | f41457c8a1aefbb09ff45cbee3083f9111525686 | ### Contracts Description Table @@ -15,9 +15,10 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleMaxTotalSupplyOwnable2Step** | Implementation | RuleMaxTotalSupplyBase, Ownable2Step ||| +| **RuleMaxTotalSupplyOwnable2Step** | Implementation | RuleMaxTotalSupplyBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleMaxTotalSupplyBase Ownable | | └ | _authorizeMaxTotalSupplyManager | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | ### Legend diff --git a/doc/surya/surya_report/surya_report_RuleMintAllowance.sol.md b/doc/surya/surya_report/surya_report_RuleMintAllowance.sol.md new file mode 100644 index 0000000..9bcd911 --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleMintAllowance.sol.md @@ -0,0 +1,31 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/RuleMintAllowance.sol | 20af9da8f7f75bbedd817de955e768f9a716b54c | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleMintAllowance** | Implementation | AccessControlModuleStandalone, RuleMintAllowanceBase, ERC3643ComplianceRolesStorage ||| +| └ | | Public ❗️ | 🛑 | AccessControlModuleStandalone | +| └ | supportsInterface | Public ❗️ | |NO❗️ | +| └ | _authorizeSetMintAllowance | Internal 🔒 | | onlyRole | +| └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyRole | +| └ | _authorizeComplianceBindingChange | Internal 🔒 | | onlyRole | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleMintAllowanceBase.sol.md b/doc/surya/surya_report/surya_report_RuleMintAllowanceBase.sol.md new file mode 100644 index 0000000..530992f --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleMintAllowanceBase.sol.md @@ -0,0 +1,45 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/abstract/RuleMintAllowanceBase.sol | 1676c17359978a8e901da2f022762462a44a7968 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleMintAllowanceBase** | Implementation | VersionModule, ERC3643ComplianceModule, RuleMintAllowanceInvariantStorage, IRule ||| +| └ | _authorizeSetMintAllowance | Internal 🔒 | | | +| └ | canReturnTransferRestrictionCode | External ❗️ | |NO❗️ | +| └ | created | External ❗️ | 🛑 | onlyBoundToken | +| └ | destroyed | External ❗️ | 🛑 | onlyBoundToken | +| └ | setMintAllowance | Public ❗️ | 🛑 | onlyAllowanceOperator | +| └ | increaseMintAllowance | Public ❗️ | 🛑 | onlyAllowanceOperator | +| └ | decreaseMintAllowance | Public ❗️ | 🛑 | onlyAllowanceOperator | +| └ | bindToken | Public ❗️ | 🛑 | onlyComplianceManager | +| └ | messageForTransferRestriction | Public ❗️ | |NO❗️ | +| └ | transferred | Public ❗️ | 🛑 | onlyBoundToken | +| └ | transferred | Public ❗️ | 🛑 | onlyBoundToken | +| └ | detectTransferRestriction | Public ❗️ | |NO❗️ | +| └ | detectTransferRestrictionFrom | Public ❗️ | |NO❗️ | +| └ | canTransfer | Public ❗️ | |NO❗️ | +| └ | canTransferFrom | Public ❗️ | |NO❗️ | +| └ | _detectTransferRestrictionFrom | Internal 🔒 | | | +| └ | _transferred | Internal 🔒 | 🛑 | | +| └ | _transferredFrom | Internal 🔒 | 🛑 | | +| └ | _setMintAllowance | Internal 🔒 | 🛑 | | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleMintAllowanceInvariantStorage.sol.md b/doc/surya/surya_report/surya_report_RuleMintAllowanceInvariantStorage.sol.md new file mode 100644 index 0000000..8f8e894 --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleMintAllowanceInvariantStorage.sol.md @@ -0,0 +1,26 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol | 99b25d9540492defbc4168b3f07b47498a07f081 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleMintAllowanceInvariantStorage** | Implementation | RuleSharedInvariantStorage ||| + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleMintAllowanceOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleMintAllowanceOwnable2Step.sol.md new file mode 100644 index 0000000..171f23a --- /dev/null +++ b/doc/surya/surya_report/surya_report_RuleMintAllowanceOwnable2Step.sol.md @@ -0,0 +1,31 @@ +## Sūrya's Description Report + +### Files Description Table + + +| File Name | SHA-1 Hash | +|-------------|--------------| +| ./rules/operation/RuleMintAllowanceOwnable2Step.sol | 32837216a0005192e2bfbe7a91ab7bd782a32238 | + + +### Contracts Description Table + + +| Contract | Type | Bases | | | +|:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| +| └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | +|||||| +| **RuleMintAllowanceOwnable2Step** | Implementation | RuleMintAllowanceBase, Ownable2Step, Ownable2StepERC165Module ||| +| └ | | Public ❗️ | 🛑 | Ownable | +| └ | supportsInterface | Public ❗️ | |NO❗️ | +| └ | _authorizeSetMintAllowance | Internal 🔒 | | onlyOwner | +| └ | _onlyComplianceManager | Internal 🔒 | 🛑 | onlyOwner | +| └ | _authorizeComplianceBindingChange | Internal 🔒 | | onlyOwner | + + +### Legend + +| Symbol | Meaning | +|:--------:|-----------| +| 🛑 | Function can modify state | +| 💵 | Function is payable | diff --git a/doc/surya/surya_report/surya_report_RuleSanctionsListOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleSanctionsListOwnable2Step.sol.md index 2ad4f13..e9cf3c0 100644 --- a/doc/surya/surya_report/surya_report_RuleSanctionsListOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleSanctionsListOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleSanctionsListOwnable2Step.sol | 8e57be4539873118b38e561331a5daa212bc68f3 | +| ./rules/validation/deployment/RuleSanctionsListOwnable2Step.sol | 0885c8f3fd637c7131ce04cc5a736b9acb60c616 | ### Contracts Description Table @@ -15,9 +15,10 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleSanctionsListOwnable2Step** | Implementation | RuleSanctionsListBase, Ownable2Step ||| +| **RuleSanctionsListOwnable2Step** | Implementation | RuleSanctionsListBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleSanctionsListBase Ownable | | └ | _authorizeSanctionListManager | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_RuleSpenderWhitelistBase.sol.md b/doc/surya/surya_report/surya_report_RuleSpenderWhitelistBase.sol.md index 6c40c5f..84c327d 100644 --- a/doc/surya/surya_report/surya_report_RuleSpenderWhitelistBase.sol.md +++ b/doc/surya/surya_report/surya_report_RuleSpenderWhitelistBase.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/abstract/base/RuleSpenderWhitelistBase.sol | 4dcfe0d0236ac5c0a1773ae39504f384433ad4c9 | +| ./rules/validation/abstract/base/RuleSpenderWhitelistBase.sol | 6a5f291ea67c01dc192a26ca6e51e0034aca9621 | ### Contracts Description Table diff --git a/doc/surya/surya_report/surya_report_RuleSpenderWhitelistOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleSpenderWhitelistOwnable2Step.sol.md index 8ab7606..5006e91 100644 --- a/doc/surya/surya_report/surya_report_RuleSpenderWhitelistOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleSpenderWhitelistOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol | 3a1a7f3c6cc386f57c7479adf4c1b5d13854a37e | +| ./rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol | f45ba40d563a6f073f666cd1cd3866efab959081 | ### Contracts Description Table @@ -15,10 +15,11 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleSpenderWhitelistOwnable2Step** | Implementation | RuleSpenderWhitelistBase, Ownable2Step ||| +| **RuleSpenderWhitelistOwnable2Step** | Implementation | RuleSpenderWhitelistBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleSpenderWhitelistBase Ownable | | └ | _authorizeAddressListAdd | Internal 🔒 | | onlyOwner | | └ | _authorizeAddressListRemove | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_RuleWhitelistBase.sol.md b/doc/surya/surya_report/surya_report_RuleWhitelistBase.sol.md index 1718595..af2048c 100644 --- a/doc/surya/surya_report/surya_report_RuleWhitelistBase.sol.md +++ b/doc/surya/surya_report/surya_report_RuleWhitelistBase.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/abstract/base/RuleWhitelistBase.sol | 3dc6bbac3c9d6bbbb012578f4d0ecd1fd790f44c | +| ./rules/validation/abstract/base/RuleWhitelistBase.sol | c03f8b0e8af1c93b60cbde46d512b75eb501c805 | ### Contracts Description Table diff --git a/doc/surya/surya_report/surya_report_RuleWhitelistOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleWhitelistOwnable2Step.sol.md index 346c5a3..0c142c3 100644 --- a/doc/surya/surya_report/surya_report_RuleWhitelistOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleWhitelistOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleWhitelistOwnable2Step.sol | a8f5ed118679dd2e32abc7e51cd7e9c165879376 | +| ./rules/validation/deployment/RuleWhitelistOwnable2Step.sol | 6c451cec10719b78b486fff1254daaf78d3c3a25 | ### Contracts Description Table @@ -15,11 +15,12 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleWhitelistOwnable2Step** | Implementation | RuleWhitelistBase, Ownable2Step ||| +| **RuleWhitelistOwnable2Step** | Implementation | RuleWhitelistBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleWhitelistBase Ownable | | └ | _authorizeAddressListAdd | Internal 🔒 | | onlyOwner | | └ | _authorizeAddressListRemove | Internal 🔒 | | onlyOwner | | └ | _authorizeCheckSpenderManager | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_RuleWhitelistWrapper.sol.md b/doc/surya/surya_report/surya_report_RuleWhitelistWrapper.sol.md index 92397aa..f69c876 100644 --- a/doc/surya/surya_report/surya_report_RuleWhitelistWrapper.sol.md +++ b/doc/surya/surya_report/surya_report_RuleWhitelistWrapper.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleWhitelistWrapper.sol | 3294eb2b561b5055686ff1eb82b1f88494d1d9c6 | +| ./rules/validation/deployment/RuleWhitelistWrapper.sol | a2aa47367533733b60d7aca73921f7e3546424b7 | ### Contracts Description Table @@ -15,12 +15,13 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleWhitelistWrapper** | Implementation | RuleWhitelistWrapperBase, AccessControlModuleStandalone ||| +| **RuleWhitelistWrapper** | Implementation | RuleWhitelistWrapperBase, AccessControlModuleStandalone, RulesManagementModuleRolesStorage ||| | └ | | Public ❗️ | 🛑 | RuleWhitelistWrapperBase AccessControlModuleStandalone | | └ | hasRole | Public ❗️ | |NO❗️ | | └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _authorizeCheckSpenderManager | Internal 🔒 | 🛑 | onlyRole | | └ | _onlyRulesManager | Internal 🔒 | 🛑 | onlyRole | +| └ | _onlyRulesLimitManager | Internal 🔒 | 🛑 | onlyRole | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_RuleWhitelistWrapperBase.sol.md b/doc/surya/surya_report/surya_report_RuleWhitelistWrapperBase.sol.md index 948fa65..ddbc88a 100644 --- a/doc/surya/surya_report/surya_report_RuleWhitelistWrapperBase.sol.md +++ b/doc/surya/surya_report/surya_report_RuleWhitelistWrapperBase.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/abstract/base/RuleWhitelistWrapperBase.sol | ae2d97261413a3a126dde152a60bef8627ad1299 | +| ./rules/validation/abstract/base/RuleWhitelistWrapperBase.sol | f345ef26fdc628576e5f4497642bd782d5f6d709 | ### Contracts Description Table diff --git a/doc/surya/surya_report/surya_report_RuleWhitelistWrapperOwnable2Step.sol.md b/doc/surya/surya_report/surya_report_RuleWhitelistWrapperOwnable2Step.sol.md index 352de2c..dbd4144 100644 --- a/doc/surya/surya_report/surya_report_RuleWhitelistWrapperOwnable2Step.sol.md +++ b/doc/surya/surya_report/surya_report_RuleWhitelistWrapperOwnable2Step.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol | 5ec7ee305486a7edfdc2dc045d717fc8ecb2b577 | +| ./rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol | daca724ea3684221c6205d7c4784c48e5ed73074 | ### Contracts Description Table @@ -15,10 +15,12 @@ |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | └ | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| -| **RuleWhitelistWrapperOwnable2Step** | Implementation | RuleWhitelistWrapperBase, Ownable2Step ||| +| **RuleWhitelistWrapperOwnable2Step** | Implementation | RuleWhitelistWrapperBase, Ownable2Step, Ownable2StepERC165Module ||| | └ | | Public ❗️ | 🛑 | RuleWhitelistWrapperBase Ownable | | └ | _authorizeCheckSpenderManager | Internal 🔒 | | onlyOwner | | └ | _onlyRulesManager | Internal 🔒 | | onlyOwner | +| └ | _onlyRulesLimitManager | Internal 🔒 | | onlyOwner | +| └ | supportsInterface | Public ❗️ | |NO❗️ | | └ | _msgSender | Internal 🔒 | | | | └ | _msgData | Internal 🔒 | | | | └ | _contextSuffixLength | Internal 🔒 | | | diff --git a/doc/surya/surya_report/surya_report_VersionModule.sol.md b/doc/surya/surya_report/surya_report_VersionModule.sol.md index e3a4204..0cf6879 100644 --- a/doc/surya/surya_report/surya_report_VersionModule.sol.md +++ b/doc/surya/surya_report/surya_report_VersionModule.sol.md @@ -5,7 +5,7 @@ | File Name | SHA-1 Hash | |-------------|--------------| -| ./modules/VersionModule.sol | 419acdca74542295a72abbf1fd6c29fdfbfe4f70 | +| ./modules/VersionModule.sol | 3351ae4920df4e78c4551e06c79154d8f8b70814 | ### Contracts Description Table diff --git a/doc/technical/INVARIANT_TESTS.md b/doc/technical/INVARIANT_TESTS.md new file mode 100644 index 0000000..217e44c --- /dev/null +++ b/doc/technical/INVARIANT_TESTS.md @@ -0,0 +1,165 @@ +# Invariant Tests + +[TOC] + +This document describes the **stateful invariant suite** in [`test/invariant/`](../../test/invariant/) — what each invariant asserts, why it matters, how the handlers are built, and how the suite was verified to actually catch bugs. + +Invariant tests differ from the unit and fuzz tests elsewhere in `test/`: instead of exercising a fixed call sequence, Foundry drives a **handler** contract with long, randomly-ordered sequences of calls and re-checks every `invariant_*` function after each step. They are the right tool for the two **stateful (operation) rules**, whose storage evolves across calls. + +Validation rules are read-only and hold no per-transfer state, so they have nothing to conserve across a call sequence — they are covered by unit and fuzz tests instead. + +--- + +## 1. Running the suite + +```bash +forge test --match-path "test/invariant/*" # the invariant suite only +forge test --match-contract ConditionalTransferInvariants +forge test --match-contract MintAllowanceInvariants +forge test # everything, invariants included +``` + +Configuration lives in `foundry.toml`: + +```toml +[invariant] +runs = 64 # independent random sequences +depth = 128 # calls per sequence +fail_on_revert = true # a reverting handler call fails the run +``` + +`runs × depth` ⇒ **8 192 handler calls per invariant**. `fail_on_revert = true` is deliberate: the handlers are written to only ever make calls the rule will accept, so **any** revert means the handler (or the rule) is wrong, and we want to know. The suite currently reports **0 reverts**. + +--- + +## 2. Architecture — the handler pattern + +Foundry cannot usefully fuzz a rule directly: `approveTransfer` needs `OPERATOR_ROLE`, `transferred` may only be called by the bound entity, and random inputs would mostly revert. So each rule gets a **handler** that: + +1. **Holds the required roles and is itself the bound entity.** The handler is passed to `bindToken(address(handler))` and granted the operator role, so `msg.sender` inside the rule is the handler and every call is authorized. +2. **Bounds the inputs.** A small actor set (3 addresses) and a small value range make the fuzzer *collide* on the same keys repeatedly, which is what actually exercises the accounting. +3. **Skips calls that would revert** (e.g. cancelling a non-existent approval), so `fail_on_revert = true` stays meaningful. +4. **Maintains ghost variables** — an independent, off-chain-style mirror of what the rule's state *should* be. The invariant then compares the rule against the ghost. + +``` + ┌──────────────────────┐ randomly-ordered calls ┌──────────────────┐ + │ Foundry invariant │ ──────────────────────────▶ │ Handler │ + │ fuzzer │ │ (bound entity, │ + └──────────────────────┘ │ role holder) │ + │ └────────┬─────────┘ + │ after every call real call │ ghost update + ▼ ▼ + ┌──────────────────────┐ ┌────────┐ ┌────────────┐ + │ invariant_*() │ compares ─────────────▶│ Rule │ │ ghosts │ + └──────────────────────┘ └────────┘ └────────────┘ +``` + +`targetSelector` restricts the fuzzer to the handler's own action functions. Without it, Foundry would also call the public functions the handler inherits from forge-std's `Test`, wasting the call budget. + +| File | Role | +|---|---| +| [`test/invariant/ConditionalTransferHandler.sol`](../../test/invariant/ConditionalTransferHandler.sol) | Drives `RuleConditionalTransferLight`'s approval state machine | +| [`test/invariant/MintAllowanceHandler.sol`](../../test/invariant/MintAllowanceHandler.sol) | Drives `RuleMintAllowance`'s quota accounting | +| [`test/invariant/RuleInvariants.t.sol`](../../test/invariant/RuleInvariants.t.sol) | The two invariant test contracts and their `setUp` | + +--- + +## 3. The invariants + +### 3.1 `RuleConditionalTransferLight` — approval conservation + +Handler actions: `approve` · `cancel` · `execute` · `executeMintOrBurn`. +Ghosts: `totalApproved`, `totalCancelled`, `totalExecuted`, `mintBurnCalls`. + +#### `invariant_approvalConservation` + +``` +totalApproved − totalCancelled − totalExecuted == Σ approvalCounts +``` + +Every approval ever recorded is, at any moment, in exactly one of three states: **still outstanding**, **cancelled**, or **consumed by a transfer**. The equality says approvals are neither **double-spent** (one `approveTransfer` consumed twice) nor **lost** (an approval that vanishes without being cancelled or used). + +The invariant additionally asserts `totalApproved >= totalCancelled + totalExecuted`, which fails loudly if the rule ever lets more approvals be consumed than were recorded — i.e. an underflow of `approvalCounts` (`INV-5`). + +#### `invariant_noApprovalExceedsTotalRecorded` + +``` +Σ approvalCounts ≤ totalApproved +``` + +A weaker but independent bound: no tuple can ever hold more outstanding approvals than were granted in total. It catches a class of bug (spurious increments) that conservation alone could mask if a matching spurious decrement existed. + +#### Emergent property — mint/burn never consume an approval + +The handler calls `executeMintOrBurn`, firing `transferred(address(0), to, v)` and `transferred(from, address(0), v)`, but **deliberately does not** count these in `totalExecuted`. If a mint or burn ever consumed an approval, `Σ approvalCounts` would drop while `totalExecuted` stayed put, and `invariant_approvalConservation` would break. + +So the mint/burn exemption is proved by the conservation invariant itself — no separate test needed. + +### 3.2 `RuleMintAllowance` — exact quota accounting + +Handler actions: `setAllowance` · `increase` · `decrease` · `mint` · `regularTransfer`. +Ghosts: `ghostAllowance[minter]` (a full mirror), `totalCredited`, `totalMinted`. + +#### `invariant_allowanceMatchesGhost` + +``` +for every minter m: rule.mintAllowance(m) == ghostAllowance[m] +``` + +The strongest of the four. The handler recomputes the expected allowance independently after every accepted operation, so this asserts the rule's arithmetic is **exactly** right after *any* interleaving of set / increase / decrease / mint. It subsumes "never underflows" and "monotonically non-increasing across mints" (`INV-7`). + +#### `invariant_mintedNeverExceedsCredited` + +``` +Σ minted ≤ Σ credited +``` + +A cumulative safety bound independent of the mirror: across the whole run, minters can never mint more in total than was ever granted to them, regardless of how quotas were reset or adjusted along the way. + +#### Emergent property — non-mint transfers never touch a quota + +The handler calls `regularTransfer` (a `transferred(spender, from ≠ 0, to, v)` call) and **deliberately leaves the ghost unchanged**. If the rule ever deducted quota on a non-mint path, the mirror would diverge and `invariant_allowanceMatchesGhost` would fail. + +--- + +## 4. Negative controls — proving the suite can fail + +An invariant suite that cannot fail is worthless. Both invariants were **mutation-tested**: a real bug was injected into the rule, the suite was run, and the failure was confirmed. The mutations were then reverted. + +| Mutation | Injected bug | Result | +|---|---|---| +| `RuleConditionalTransferLightApprovalBase._transferred` — remove `approvalCounts[hash] = count - 1` | Approval **double-spend**: one approval can be consumed forever | ❌ `invariant_approvalConservation` fails: `approval accounting drifted: 0 != 1` | +| `RuleMintAllowanceBase._transferredFrom` — `current - value` → `current - value + 1` | **Off-by-one** quota deduction: minters slowly gain free quota | ❌ `invariant_allowanceMatchesGhost` fails: `mint allowance drifted from expected: 1938542 != 1938541` | + +Re-run these yourself before trusting a change to either rule: if you mutate the accounting and the suite still passes, the suite has regressed. + +--- + +## 5. Coverage map + +Invariant IDs refer to [`THREAT_MODEL.md`](../../THREAT_MODEL.md) §8; verification status is tracked in [`RESULT.md`](../../RESULT.md). + +| Invariant (threat model) | Property | Covered by | +|---|---|---| +| `INV-5` | `approvalCounts` never underflows; one approval ⇒ one transfer | `invariant_approvalConservation`, `invariant_noApprovalExceedsTotalRecorded` | +| `INV-7` | `mintAllowance` non-increasing across mints, never underflows, `Σ minted ≤ Σ granted` | `invariant_allowanceMatchesGhost`, `invariant_mintedNeverExceedsCredited` | +| `INV-12` (partial) | Mint/burn handled explicitly, never falling into the transfer path | Emergent from both suites (see §3.1, §3.2) | + +**Not covered by invariants (by design):** + +- `INV-1`, `INV-2`, `INV-3`, `INV-9` — properties of *stateless* view functions; unit and fuzz tests are the right tool (`test/ThreatModel/ThreatModelTests.t.sol`). +- `INV-6` (`_transferHash` injectivity) — a pure function; covered by `testFuzz_HASH1_ApprovalBucketsAreDistinct`. +- `INV-4`, `INV-11` (access control) — covered by the per-rule access-control suites. +- `INV-10` (ERC-2771 binding identity) — currently holds by static reasoning; a live regression test is the open item **I-10a** in [`RULE_IMPROVEMENT.md`](../../RULE_IMPROVEMENT.md). + +--- + +## 6. Adding a new invariant + +1. Add the action to the relevant handler. **Guard it** so it can only make calls the rule accepts (`fail_on_revert = true` will otherwise fail the run). +2. Update the ghost state *after* the rule call, in the same function. If the rule call reverts, the whole handler call reverts and the ghost rolls back with it — which is what keeps the mirror consistent. +3. Register the new selector in the `targetSelector(...)` array in `RuleInvariants.t.sol`, otherwise it will never be fuzzed. +4. Add the `invariant_*` function, with a message argument on each assertion so a failure is legible. +5. **Mutation-test it.** Inject the bug it is supposed to catch and confirm it fails. + +If you add a new stateful rule, it needs its own handler; validation rules do not. diff --git a/doc/technical/RULE_SEMANTICS.md b/doc/technical/RULE_SEMANTICS.md new file mode 100644 index 0000000..f5ff71d --- /dev/null +++ b/doc/technical/RULE_SEMANTICS.md @@ -0,0 +1,102 @@ +# Rule Semantics — comparison table + +[TOC] + +The rules in this library answer the same two questions — *may this transfer proceed?* (read path) and *record/enforce it* (write path) — but they answer with **different conventions**. In particular they differ on **whether they screen the spender**, **how they treat mint (`from == address(0)`) and burn (`to == address(0)`)**, and **what happens when their external oracle/registry is unset**. This page is the single place those differences are laid out side by side so an integrator does not have to read each rule's source to learn them. + +Each cell reflects the rule's own `_detectTransferRestriction*` logic; behaviour is identical on the enforcement path (`transferred`), which reverts on any non-`TRANSFER_OK` code. + +Legend: ✅ screened / can block · ❌ not screened · ⚙️ conditional (see note) · n/a not applicable. + +--- + +## 1. Who each rule screens + +"Direct" = a plain `transfer` (`transferred(from, to, value)`). The spender columns apply to the 4-arg `transferred(spender, from, to, value)` path; **mint** is that path with `from == address(0)` and `spender == minter`, **burn** with `to == address(0)` and `spender == burner` (CMTAT v3.3+). + +| Rule | `from` (direct) | `to` (direct) | spender on `transferFrom` | spender on **mint** | spender on **burn** | +|---|---|---|---|---|---| +| `RuleWhitelist` | ✅ must be listed | ✅ must be listed | ⚙️ only if `checkSpender` | ❌ exempt | ❌ exempt | +| `RuleWhitelistWrapper` | ✅ listed in ≥1 child | ✅ listed in ≥1 child | ⚙️ only if `checkSpender` | ❌ exempt | ❌ exempt | +| `RuleSpenderWhitelist` | ❌ always allowed | ❌ always allowed | ✅ always (rule's purpose) | ❌ exempt | ❌ exempt | +| `RuleBlacklist` | ✅ blocks if listed | ✅ blocks if listed | ✅ blocks if listed | ✅ blocks listed minter [1] | ✅ blocks listed burner [1] | +| `RuleSanctionsList` | ✅ blocks if sanctioned | ✅ blocks if sanctioned | ✅ blocks if sanctioned | ✅ blocks sanctioned minter [1] | ✅ blocks sanctioned burner [1] | +| `RuleMaxTotalSupply` | ⚙️ mint only [2] | ❌ | ❌ ignored | ❌ caps supply, not minter | ❌ | +| `RuleIdentityRegistry` | ⚙️ only if `checkSender` [3] | ✅ **must be verified** (ERC-3643) [3] | ⚙️ only if `checkSpender` [3] | ❌ exempt [3] | ❌ exempt [3] | +| `RuleERC2980` | ⚙️ frozen-check only [4] | ✅ frozen-check + must be whitelisted | ✅ frozen-check | ✅ blocks frozen minter | ✅ blocks frozen burner | +| `RuleConditionalTransferLight` | ❌ per-tuple approval [5] | ❌ per-tuple approval [5] | ❌ spender ignored | ❌ exempt | ❌ exempt | +| `RuleConditionalTransferLightMultiToken` | ❌ per-tuple approval [5] | ❌ per-tuple approval [5] | ❌ spender ignored | ❌ exempt | ❌ exempt | +| `RuleMintAllowance` | ❌ not tracked | ❌ not tracked | ❌ not tracked | ✅ **debits minter quota** [6] | ❌ not tracked | + +## 2. Operational characteristics + +| Rule | When its oracle/registry is unset | Stateful on transfer? [7] | Authoritative pre-flight view | Restriction codes | +|---|---|---|---|---| +| `RuleWhitelist` | n/a (local address set) | ❌ | `canTransfer` / `canTransferFrom` | 21–25 | +| `RuleWhitelistWrapper` | empty wrapper ⇒ **all rejected** (fail-closed) | ❌ | `canTransfer` / `canTransferFrom` | 21–25 | +| `RuleSpenderWhitelist` | n/a (local address set) | ❌ | `canTransfer` (always ✓) / `canTransferFrom` | 66 | +| `RuleBlacklist` | n/a (local address set) | ❌ | `canTransfer` / `canTransferFrom` | 36–38 | +| `RuleSanctionsList` | oracle == 0 ⇒ **all allowed** (fail-open) [8] | ❌ | `canTransfer` / `canTransferFrom` | 30–32 | +| `RuleMaxTotalSupply` | token contract required (non-zero) | ❌ | `canTransfer` / `canTransferFrom` [9] | 50 | +| `RuleIdentityRegistry` | registry == 0 ⇒ **all allowed** (fail-open) [8] | ❌ | `canTransfer` / `canTransferFrom` | 55–57 | +| `RuleERC2980` | n/a (local lists) | ❌ | `canTransfer` / `canTransferFrom` | 60–65 | +| `RuleConditionalTransferLight` | n/a (needs `bindToken`) | ✅ consumes an approval | `canTransfer` / `canTransferFrom` | 46 | +| `RuleConditionalTransferLightMultiToken` | n/a (needs `bindToken`) | ✅ consumes an approval | `canTransferForToken` / `detectTransferRestrictionForToken` [10] | 46 | +| `RuleMintAllowance` | n/a (needs `bindToken`) | ✅ debits quota | ⚠️ `canTransferFrom` **only** [11] | 70 | + +--- + +## 3. Overload surface (ERC-7943 `tokenId` / `ITransferContext`) + +Not every rule exposes the same entrypoints. The ERC-7943 `tokenId` overloads and the `ITransferContext` struct entrypoints come from `RuleNFTAdapter`, and **only the rules that inherit it have them**. This is a deliberate design choice, not an oversight: `RuleMaxTotalSupply` caps a fungible supply, and the conditional-transfer / mint-allowance rules key on fungible amounts, so a `tokenId` dimension would be meaningless for them. + +| Rule | ERC-7943 `tokenId` overloads [12] | `transferred(FungibleTransferContext)` | `transferred(MultiTokenTransferContext)` | +|---|---|---|---| +| `RuleWhitelist` | ✅ | ✅ | ✅ | +| `RuleWhitelistWrapper` | ✅ | ✅ | ✅ | +| `RuleBlacklist` | ✅ | ✅ | ✅ | +| `RuleSpenderWhitelist` | ✅ | ✅ | ✅ | +| `RuleSanctionsList` | ✅ | ✅ | ✅ | +| `RuleERC2980` | ✅ | ✅ | ✅ | +| `RuleIdentityRegistry` | ✅ | ✅ | ✅ | +| `RuleMaxTotalSupply` | ❌ | ❌ | ❌ | +| `RuleConditionalTransferLight` | ❌ | ✅ | ❌ | +| `RuleConditionalTransferLightMultiToken` | ❌ | ✅ | ❌ | +| `RuleMintAllowance` | ❌ | ❌ | ❌ | + +The `tokenId` parameter is **always ignored** by the rules that accept it — `RuleNFTAdapter` exists purely to re-expose the same restriction logic under the ERC-7943 signatures. The `tokenId` overload of any function therefore returns exactly what its fungible counterpart returns, and the `ctx` entrypoints dispatch to the same internal hooks (`ctx.sender == 0` or `ctx.sender == ctx.from` ⇒ the direct hook; otherwise the spender-aware hook). This parity is asserted for every rule above in `test/TransferContext/OverloadParity.t.sol`. + +**Access control on the `ctx` entrypoints (threat `AC-5`).** `transferred(FungibleTransferContext)` / `transferred(MultiTokenTransferContext)` are `external` with **no caller restriction** on the validation rules. That is safe because those rules' hooks are `view`: an arbitrary caller can run the check and be reverted by it, but cannot mutate any state. The stateful multi-token rule guards its own `ctx` entrypoint with `onlyTransferExecutor`. + +## 4. Notes & caveats + +> **Mint/burn permission is an explicit flag, never `address(0)` list membership.** `RuleWhitelist`, `RuleWhitelistWrapper` and `RuleERC2980` each expose `allowMint` / `allowBurn` — set together by the `allowMintBurn` constructor parameter, then independently settable via `setAllowMint` / `setAllowBurn` (e.g. to permanently close issuance while keeping redemptions open). The zero address can **never** enter any list (single adds revert, batch adds skip it), so the standardized getters stay truthful: `isVerified(address(0))` and `whitelist(address(0))` are always `false`, as ERC-3643 and ERC-2980 require. A blocked mint returns a dedicated code (`24` for the whitelist rules, `64` for ERC-2980) rather than the misleading "sender not whitelisted". The flag gates the **operation only**: a permitted mint still requires a whitelisted *recipient*, and a permitted burn a whitelisted *sender*. + + +1. **Deny-lists intentionally screen the minter/burner.** `RuleBlacklist` and `RuleSanctionsList` do **not** exempt mint/burn from the spender check, so a blacklisted/sanctioned address cannot mint or burn. This is correct fail-closed behaviour for a deny-list (threat `BL-1`), the mirror image of the whitelist rules, which exempt mint/burn because the minter acts on its own authority rather than as a delegated spender. + +2. **`RuleMaxTotalSupply` only acts on mints.** `_detectTransferRestriction` returns `TRANSFER_OK` unless `from == address(0)`; it caps *total supply*, so the "screened party" is the mint operation, not any address. The spender is ignored on every path. + +3. **`RuleIdentityRegistry` is ERC-3643 conformant: only the RECEIVER is verified** (improvement I-1, finding **F-1** fixed). The spec mandates exactly one check — *"The receiver MUST be whitelisted on the Identity Registry and verified"* — and explicitly states that `transferFrom` "works the same way", that `mint` "only require[s] the receiver", and that `burn` "bypasses all checks on eligibility". The sender, spender and minter are therefore **not** screened by default. Checking the sender would **trap de-listed holders**: ERC-3643 screens only the receiver precisely so an investor whose identity lapses can still exit their position by sending to a verified counterparty. Stricter screening is available as an explicit opt-in via `checkSender` / `checkSpender` (both default `false`); mint and burn stay exempt from the spender check even when `checkSpender` is on. + +4. **`RuleERC2980` does not require the sender to be whitelisted** — only that the sender is *not frozen*; only the recipient must be whitelisted (threat `E29-1`, ERC-2980 semantics). Note also that freezing `address(0)` blocks all mints and that burns require `address(0)` to be whitelisted via the `allowBurn` constructor flag (threat `E29-2`). + +5. **Conditional-transfer rules screen the (from, to, value) tuple, not identities.** A transfer is allowed iff an operator has recorded an approval for that exact tuple; the individual addresses are never checked against a list. Mint/burn are exempt (`from`/`to == address(0)` returns early). + +6. **`RuleMintAllowance` is the only rule that *uses* the mint spender.** On the 4-arg path with `from == address(0)`, it debits `mintAllowance[spender]`. On the 3-arg path (no spender) it performs no deduction, so it must be deployed against the CMTAT/RuleEngine v3.3+ spender-aware path. + +7. **Stateful** means the rule writes storage inside the `transferred` callback. Validation rules are read-only; the three operation rules (`RuleConditionalTransferLight`, `…MultiToken`, `RuleMintAllowance`) mutate state and require `bindToken`. + +8. **Fail-open when unset.** `RuleSanctionsList` (oracle == `address(0)`) and `RuleIdentityRegistry` (registry == `address(0)`) return `TRANSFER_OK` for everything — screening is disabled, not fail-closed (threats `SL-1`/`SL-2`). `clearSanctionListOracle()` / `clearIdentityRegistry()` are single-call kill switches for that screening. Compose with another rule if a hard floor is required. + +9. **`RuleMaxTotalSupply` views are overflow-safe** (finding **F-2**, fixed): `detectTransferRestriction` / `canTransfer` return code `50` instead of reverting when `currentSupply + value` would overflow. + +10. **`RuleConditionalTransferLightMultiToken` is direct-binding-only, and its `detectTransferRestriction` depends on `msg.sender`.** Approvals are recorded under the `token` argument but *consumed* under `msg.sender`, so the rule **must be bound directly to each token** (`CMTAT.setRuleEngine(rule)`) and **must not be added to a `RuleEngine`** — behind an engine it either reverts or silently loses all per-token isolation (finding **F-4**; full case analysis in [RuleConditionalTransferLightMultiToken.md](./RuleConditionalTransferLightMultiToken.md#deployment-topology--why-a-ruleengine-does-not-work)). For the same reason `detectTransferRestriction` / `canTransfer` derive the token key from the caller, so an off-chain `eth_call` from a non-bound address always reads "not approved" (code 46) even for an approved transfer (threat `CTL-4`, finding **F-8**). Use the caller-explicit **`detectTransferRestrictionForToken(token, …)`** / **`canTransferForToken(token, …)`** views for pre-flight — they take the token as a parameter and give every caller the real answer. + +11. **`RuleMintAllowance.canTransfer` / `detectTransferRestriction` are NOT authoritative** (finding **F-7**): they are hardcoded to "allowed" because the 3-arg signature has no minter identity. Pre-flight a mint with `canTransferFrom(minter, address(0), to, value)`. See [RuleMintAllowance.md](./RuleMintAllowance.md#eligibility-views-which-one-is-authoritative). + +12. **The ERC-7943 `tokenId` overloads** are `detectTransferRestriction(from,to,tokenId,value)`, `detectTransferRestrictionFrom(spender,from,to,tokenId,value)`, `canTransfer(from,to,tokenId,amount)`, `canTransferFrom(spender,from,to,tokenId,value)`, `transferred(from,to,tokenId,value)` and `transferred(spender,from,to,tokenId,value)` — all supplied by `RuleNFTAdapter`. Per ERC-7943, `amount`/`value` MUST be `1` for ERC-721. The rules ignore `tokenId` entirely; it exists so an ERC-721/ERC-1155 token can call the same compliance rule without a shim. + +--- + +See [`../../RESULT.md`](../../RESULT.md) for the findings referenced above and [`../../THREAT_MODEL.md`](../../THREAT_MODEL.md) for the threat IDs. diff --git a/doc/technical/RuleBlacklist.md b/doc/technical/RuleBlacklist.md index bace50e..da320e1 100644 --- a/doc/technical/RuleBlacklist.md +++ b/doc/technical/RuleBlacklist.md @@ -16,6 +16,14 @@ A significant portion of the address-list management code is shared with the whi ![surya_inheritance_RuleBlacklist](../surya/surya_inheritance/surya_inheritance_RuleBlacklist.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer. + +![RuleBlacklist flow with a CMTAT token](../img/rule-blacklist-flow.png) + +_Diagram source: doc/img/rule-blacklist-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | diff --git a/doc/technical/RuleConditionalTransfer.md b/doc/technical/RuleConditionalTransfer.md index fcea617..c692247 100644 --- a/doc/technical/RuleConditionalTransfer.md +++ b/doc/technical/RuleConditionalTransfer.md @@ -69,11 +69,19 @@ To perform the transfer, the token holder has to `approve` the rule to spend tok ### Graph -![surya_graph_Blacklist](../surya/surya_graph/surya_graph_RuleConditionalTransfer.sol.png) +![surya_graph_RuleConditionalTransferLight](../surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png) ### Inheritance -![surya_inheritance_RuleWhitelistWrapper.sol](../surya/surya_inheritance/surya_inheritance_RuleConditionalTransfer.sol.png) +![surya_inheritance_RuleConditionalTransferLight](../surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png) + +### Flow with a CMTAT token + +The sequence below shows the full *Vinkulierung* workflow: a holder requests a transfer, an operator approves it, and the CMTAT token (with this rule configured in its RuleEngine) validates the approved request and marks it executed. The optional `AUTOMATIC_TRANSFER` path where the rule performs the transfer on approval is also shown. + +![RuleConditionalTransfer flow with a CMTAT token](../img/rule-conditional-transfer-flow.png) + +_Diagram source: doc/img/rule-conditional-transfer-flow.puml._ ### Workflow @@ -167,7 +175,7 @@ The default admin is the address put in argument(`admin`) inside the constructor ### Graph -![surya_graph_Whitelist](../surya/surya_graph/surya_graph_RuleConditionalTransfer.sol.png) +![surya_graph_RuleConditionalTransferLight](../surya/surya_graph/surya_graph_RuleConditionalTransferLight.sol.png) diff --git a/doc/technical/RuleConditionalTransferLight.md b/doc/technical/RuleConditionalTransferLight.md index 2c338b2..12ea648 100644 --- a/doc/technical/RuleConditionalTransferLight.md +++ b/doc/technical/RuleConditionalTransferLight.md @@ -18,6 +18,14 @@ Mints (`from == address(0)`) and burns (`to == address(0)`) are **exempt**: they ![surya_inheritance_RuleConditionalTransferLight](../surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLight.sol.png) +### Flow with a CMTAT token + +The sequence below shows the two-phase flow: an operator first approves a `(from, to, value)` transfer, then the CMTAT token (with this rule configured in its RuleEngine) validates and consumes that approval during the transfer. + +![RuleConditionalTransferLight flow with a CMTAT token](../img/rule-conditional-transfer-light-flow.png) + +_Diagram source: doc/img/rule-conditional-transfer-light-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | @@ -45,17 +53,66 @@ Increments the approval count for the `(from, to, value)` hash by 1. Restricted Decrements the approval count for the `(from, to, value)` hash by 1. Reverts if no approval exists. Restricted to `OPERATOR_ROLE`. Emits `TransferApprovalCancelled`. +### `resetApproval(address from, address to, uint256 value) → uint256` + +Discards **every** outstanding approval for the `(from, to, value)` hash in one call and returns the count that was cleared. Reverts if no approval exists. Restricted to `OPERATOR_ROLE`. Emits `TransferApprovalReset`. + +Unlike `approveTransfer`, this deliberately does **not** require a token to be bound — its main use is discarding approvals that survived an `unbindToken` (see below), at which point nothing is bound. + ### `approveAndTransferIfAllowed(address from, address to, uint256 value) → bool` -Approves the transfer and immediately calls `SafeERC20.safeTransferFrom` on the currently bound token, using this rule contract as the spender. Requires `from` to have previously approved this contract for at least `value` tokens. Restricted to `OPERATOR_ROLE`. +Approves the transfer and immediately calls `SafeERC20.safeTransferFrom` on the bound token, using this rule contract as the spender. Requires `from` to have previously approved this contract for at least `value` tokens. Restricted to `OPERATOR_ROLE`. + +Works in **both** topologies, provided the bindings are set correctly — see [Binding: token vs RuleEngine](#binding-token-vs-ruleengine). ### `approvedCount(address from, address to, uint256 value) → uint256` Returns the current approval count for the `(from, to, value)` tuple. +### Binding: token vs RuleEngine + +The rule keeps **two independent bindings**, because they answer two different questions: + +| Binding | Answers | Used for | +| --- | --- | --- | +| `bindToken(token)` | *Which ERC-20 does this rule act on?* | the token `approveAndTransferIfAllowed` calls `safeTransferFrom` on — and, in direct-binding mode, an authorized caller of `transferred` | +| `bindRuleEngine(engine)` | *Who else may call `transferred`?* | under the RuleEngine topology the **engine**, not the token, is the caller of the compliance hooks | + +`transferred` accepts a call from **either** the bound token **or** the bound RuleEngine (`isTransferExecutor(caller)`). + +**Wire it like this:** + +```solidity +// Direct binding (token calls the rule itself) +cmtat.setRuleEngine(address(rule)); +rule.bindToken(address(cmtat)); + +// Behind a RuleEngine (the engine calls the rule) +cmtat.setRuleEngine(address(ruleEngine)); +ruleEngine.addRule(rule); +rule.bindToken(address(cmtat)); // the ERC-20 target +rule.bindRuleEngine(address(ruleEngine)); // the authorized caller +``` + +> **Why two bindings?** They were originally one slot, which had to be *both* the ERC-20 target and the authorized caller. In direct mode those coincide, so it worked. Behind a RuleEngine they are different addresses, and a single slot could only hold one: binding the engine broke `approveAndTransferIfAllowed` (the engine is not an ERC-20), while binding the token left the engine unauthorized and reverted **every** transfer and mint. Splitting the roles fixes both. See `RESULT.md` finding **F-3**. + +Always bind the **token** with `bindToken` — putting the RuleEngine there instead makes `getTokenBound()` a non-ERC-20 and `approveAndTransferIfAllowed` will revert. + ### `bindToken(address token)` / `unbindToken(address token)` -Binds or unbinds a token contract. Only bound tokens are authorised to call `transferred`. Restricted to `COMPLIANCE_MANAGER_ROLE`. +Binds or unbinds the ERC-20 token. Restricted to `COMPLIANCE_MANAGER_ROLE`. A second `bindToken` reverts until the current token is unbound. + +> ⚠️ **`unbindToken` does not clear `approvalCounts`.** Approvals recorded while the previous token was bound remain in storage and become consumable by the next token that is bound. The operator who controls rebinding also controls approvals, so the trust model is preserved — but when migrating to a different token, call `resetApproval` for each affected `(from, to, value)` **before** rebinding if the old approvals must not carry over. + +### `bindRuleEngine(address ruleEngine)` / `unbindRuleEngine()` + +Authorises (or revokes) a `RuleEngine` to call the transfer execution hooks. Restricted to `COMPLIANCE_MANAGER_ROLE`. Reverts on the zero address, and on a second binding until `unbindRuleEngine` is called. Emits `RuleEngineBound` / `RuleEngineUnbound`. + +Like `unbindToken`, `unbindRuleEngine` does **not** clear `approvalCounts`. + +### `isTransferExecutor(address caller) → bool` + +Returns `true` if `caller` is the bound token or the bound RuleEngine — i.e. whether it may call `transferred`. ## Workflow diff --git a/doc/technical/RuleConditionalTransferLightMultiToken.md b/doc/technical/RuleConditionalTransferLightMultiToken.md new file mode 100644 index 0000000..a668d98 --- /dev/null +++ b/doc/technical/RuleConditionalTransferLightMultiToken.md @@ -0,0 +1,135 @@ +# Rule Conditional Transfer Light MultiToken + +[TOC] + +`RuleConditionalTransferLightMultiToken` is an operation rule that requires explicit operator approval before each transfer, with approvals scoped per token. + +Approval key: + +- `keccak256(token, from, to, value)` + +This prevents approval reuse across tokens — **provided the rule receives token-specific caller context, which only happens when each token calls the rule directly.** + +> ## ⚠️ Deployment requirement: bind this rule **directly to each token** +> +> **Do not add this rule to a `RuleEngine`.** Wire it with `CMTAT.setRuleEngine(rule)` on every token, and bind each token with `bindToken(tokenA)`, `bindToken(tokenB)`, … +> +> The reason is structural: approvals are **recorded** under the `token` argument you pass to `approveTransfer`, but **consumed** under `msg.sender`. Those two keys agree only when the caller *is* the token. Behind a `RuleEngine` the caller is the engine, and the rule cannot do its job — see [Deployment topology](#deployment-topology--why-a-ruleengine-does-not-work) for the exhaustive case analysis. +> +> "MultiToken" means *several tokens each pointing directly at one shared rule* — not *one RuleEngine serving several tokens*. + +## Deployment topology — why a RuleEngine does not work + +Two guards determine what is possible: + +- `_authorizeTransferExecution()` — `require(isTokenBound(msg.sender))`: **the caller must be bound.** +- `_approveTransfer()` — `require(isTokenBound(token))`: **the `token` argument must be bound.** + +Behind a `RuleEngine` (`CMTAT.setRuleEngine(engine)` + `engine.addRule(rule)`), the engine — not the token — is the `msg.sender` of every `transferred()` call. Every possible wiring then fails: + +| # | Wiring | Outcome | +| --- | --- | --- | +| A | Nothing bound | Engine calls `transferred` → not bound → reverts `TransferExecutorUnauthorized`. Every transfer fails. | +| B | Bind the **token** | The engine is still the caller and is not bound → same revert. Binding the token achieves nothing, because the token never calls the rule. | +| C | Bind the **engine**, approve with the **token** address | `approveTransfer(token, …)` → `token` is not bound → reverts `RuleConditionalTransferLightMultiToken_InvalidToken`. The approval cannot even be recorded. | +| C′ | Bind **both**, approve with the **token** address | Approval stored under `H(token, …)`; the engine consumes under `H(engine, …)` → reverts `TransferNotApproved`, and the approval is **stranded in storage permanently**. | +| D | Bind the **engine**, approve with the **engine** address | The only configuration that runs — and it defeats the rule's purpose (see below). | + +Case D "works" but is not a valid deployment: + +1. **No per-token isolation.** The approval key is the engine, not the token. An approval recorded for `(alice → bob, 100)` intending token A is equally consumable on token B. This is exactly the cross-token approval reuse this rule exists to prevent. +2. **The `token` parameter becomes misleading.** The operator must pass the *engine* address into a parameter named `token`. Reading the API as written lands you in case C′ and strands approvals. +3. **`approveAndTransferIfAllowed` cannot work.** It calls `IERC20(token).allowance(...)` on what is actually the engine → revert. +4. **It is strictly worse than the single-token rule.** Case D is only safe when the engine serves exactly one token — and in that situation [`RuleConditionalTransferLight`](./RuleConditionalTransferLight.md) does the same job with an honest API and a working `approveAndTransferIfAllowed`. + +**Correct deployment (direct binding).** Each token calls the rule itself, so `msg.sender == tokenX`, the approve key and the consume key are both `H(tokenX, …)`, and approvals are genuinely isolated per token: + +``` +CMTAT_A.setRuleEngine(rule); rule.bindToken(address(CMTAT_A)); +CMTAT_B.setRuleEngine(rule); rule.bindToken(address(CMTAT_B)); + +rule.approveTransfer(address(CMTAT_A), alice, bob, 100); // usable on CMTAT_A only +``` + +Supporting true per-token scoping behind a `RuleEngine` would require the token address to be threaded into `IRuleEngine.transferred(...)`, which is an upstream RuleEngine interface change and is not possible from this repository. See `RESULT.md` finding **F-4**. + +## Schema + +### Graph + +![surya_graph_RuleConditionalTransferLightMultiToken](../surya/surya_graph/surya_graph_RuleConditionalTransferLightMultiToken.sol.png) + +### Inheritance + +![surya_inheritance_RuleConditionalTransferLightMultiToken](../surya/surya_inheritance/surya_inheritance_RuleConditionalTransferLightMultiToken.sol.png) + +### Flow with a CMTAT token + +The sequence below shows the two-phase flow with token-scoped approvals in the supported topology (**the rule bound directly to the token**): an operator approves a `(token, from, to, value)` transfer, then the CMTAT token validates and consumes that approval. + +![RuleConditionalTransferLightMultiToken flow with a CMTAT token](../img/rule-conditional-transfer-light-multitoken-flow.png) + +_Diagram source: doc/img/rule-conditional-transfer-light-multitoken-flow.puml._ + +## Restriction codes + +| Constant | Code | Meaning | +| --- | --- | --- | +| `CODE_TRANSFER_REQUEST_NOT_APPROVED` | 46 | No approval exists for this `(token, from, to, value)` tuple | + +## Access control + +| Role | Description | +| --- | --- | +| `DEFAULT_ADMIN_ROLE` | Manages all roles (AccessControl variant) | +| `OPERATOR_ROLE` | Approve/cancel approvals and call `approveAndTransferIfAllowed` | +| `COMPLIANCE_MANAGER_ROLE` | Bind/unbind token contracts | + +## Methods + +### `approveTransfer(address token, address from, address to, uint256 value)` + +Approves one transfer for a specific token key. + +### `cancelTransferApproval(address token, address from, address to, uint256 value)` + +Removes one approval for a specific token key. Reverts if none exists. + +### `resetApproval(address token, address from, address to, uint256 value) -> uint256` + +Discards **every** outstanding approval for the `(token, from, to, value)` key in one call and returns the count that was cleared. Reverts if none exists. Restricted to `OPERATOR_ROLE`. Emits `TransferApprovalReset`. + +Unlike `approveTransfer`, this deliberately does **not** require the token to be bound. That is what makes it usable for the two cleanup cases: approvals left behind by an `unbindToken`, and approvals stranded under a key that can never be consumed (see finding **F-4** and the [Deployment topology](#deployment-topology--why-a-ruleengine-does-not-work) section). + +### `approvedCount(address token, address from, address to, uint256 value) -> uint256` + +Returns the remaining count for a specific token key. + +### `approveAndTransferIfAllowed(address token, address from, address to, uint256 value) -> bool` + +Approves and executes `safeTransferFrom` on the specified token, requiring allowance for this rule as spender. + +### `transferred(...)` + +Only bound tokens can call transfer execution hooks. Approval consumption uses the **caller** (`msg.sender`) as the token key — which is why the rule must be bound directly to each token. See [Deployment topology](#deployment-topology--why-a-ruleengine-does-not-work). + +### `detectTransferRestrictionForToken(address token, address from, address to, uint256 value) -> uint8` + +**The view integrators should use.** Returns the restriction code for a transfer of `token`, taking the token **explicitly** instead of deriving it from `msg.sender`. Any caller — including an off-chain `eth_call` from a wallet, explorer or aggregator — gets the real answer. + +### `canTransferForToken(address token, address from, address to, uint256 value) -> bool` + +Boolean counterpart of `detectTransferRestrictionForToken`. Prefer it over `canTransfer`, which is caller-dependent. + +### `detectTransferRestriction(from, to, value)` / `canTransfer(from, to, value)` + +⚠️ **Caller-dependent — prefer the `…ForToken` views above.** These ERC-1404 / ERC-3643 views derive the token key from `msg.sender`, so they only return a meaningful answer when invoked *by the bound token*. Any other caller always receives `CODE_TRANSFER_REQUEST_NOT_APPROVED` (46) / `false`, even for a transfer that is approved and will succeed. They are fail-closed, but carry no signal for third-party pre-flight. See `RESULT.md` finding **F-8**. + +The standardized signatures are kept as-is (the token cannot be added to them without breaking ERC-1404), and both the implicit and explicit views are backed by the same internal helper, so for the bound token they can never disagree. + +## Notes + +- Mints and burns are exempt from approval consumption (`from == address(0)` or `to == address(0)`). +- This rule is ERC-20 operation-focused, like `RuleConditionalTransferLight`. +- **Do not deploy this rule behind a `RuleEngine`.** Approvals are consumed under `msg.sender`, so token scoping is lost (or the rule breaks outright) in that topology — the full case analysis is in [Deployment topology](#deployment-topology--why-a-ruleengine-does-not-work). If you need a conditional-transfer rule for a single token behind a RuleEngine, use [`RuleConditionalTransferLight`](./RuleConditionalTransferLight.md) instead. +- `unbindToken` does **not** clear `approvalCounts`. Approvals recorded for a previously bound token remain in storage and become consumable again if that token is rebound. Use `resetApproval(token, from, to, value)` to discard them before rebinding — it works on an unbound token precisely for this reason. diff --git a/doc/technical/RuleERC2980.md b/doc/technical/RuleERC2980.md index 4bf1ffb..497dc8d 100644 --- a/doc/technical/RuleERC2980.md +++ b/doc/technical/RuleERC2980.md @@ -18,6 +18,14 @@ This rule implements the [ERC-2980](https://eips.ethereum.org/EIPS/eip-2980) Swi ![surya_inheritance_RuleERC2980](../surya/surya_inheritance/surya_inheritance_RuleERC2980.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer. The frozenlist is evaluated before the recipient whitelist. + +![RuleERC2980 flow with a CMTAT token](../img/rule-erc2980-flow.png) + +_Diagram source: doc/img/rule-erc2980-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | @@ -26,6 +34,8 @@ This rule implements the [ERC-2980](https://eips.ethereum.org/EIPS/eip-2980) Swi | `CODE_ADDRESS_TO_IS_FROZEN` | 61 | Recipient is frozen | | `CODE_ADDRESS_SPENDER_IS_FROZEN` | 62 | Spender is frozen | | `CODE_ADDRESS_TO_NOT_WHITELISTED` | 63 | Recipient is not whitelisted | +| `CODE_MINT_NOT_ALLOWED` | 64 | Minting is disabled (`allowMint == false`) | +| `CODE_BURN_NOT_ALLOWED` | 65 | Burning is disabled (`allowBurn == false`) | ## Access Control @@ -39,26 +49,36 @@ This rule implements the [ERC-2980](https://eips.ethereum.org/EIPS/eip-2980) Swi ## Constructor burn configuration -- `RuleERC2980(address admin, address forwarderIrrevocable, bool allowBurn)` -- `RuleERC2980Ownable2Step(address owner, address forwarderIrrevocable, bool allowBurn)` +- `RuleERC2980(address admin, address forwarderIrrevocable, bool allowMintBurn)` +- `RuleERC2980Ownable2Step(address owner, address forwarderIrrevocable, bool allowMintBurn)` + +`allowMintBurn` sets **both** `allowMint` and `allowBurn`; they are independently settable afterwards via +`setAllowMint(bool)` / `setAllowBurn(bool)`. + +Mint/burn permission is an **explicit flag** — the constructor does **not** whitelist `address(0)`, and the zero +address can never enter either list (`addWhitelistAddress(address(0))` / `addFrozenlistAddress(address(0))` revert). +That keeps the **mandatory ERC-2980 getters** truthful: `whitelist(address(0))` and `frozenlist(address(0))` always +return `false`. -If `allowBurn` is `true`, the constructor whitelists `address(0)` so burn/redemption transfers to `address(0)` are allowed. -If `allowBurn` is `false`, `address(0)` is not whitelisted by default and burn/redemption transfers revert with `CODE_ADDRESS_TO_NOT_WHITELISTED` (`63`). +- `allowMintBurn = false` (default-safe): mint is refused with `CODE_MINT_NOT_ALLOWED` (`64`), burn with + `CODE_BURN_NOT_ALLOWED` (`65`) — dedicated codes, not the misleading "recipient not whitelisted" (`63`). +- `allowMintBurn = true`: both permitted. A permitted mint still requires the recipient to be whitelisted and not + frozen; a permitted burn still requires the sender not to be frozen. ## Whitelist methods ### `addWhitelistAddress(address targetAddress)` -Adds a single address to the whitelist. Reverts if already listed. +Adds a single address to the whitelist. Reverts with `RuleERC2980_AddressAlreadyWhitelisted` if already listed. ### `addWhitelistAddresses(address[] calldata targetAddresses)` -Batch-adds addresses to the whitelist. Silently skips duplicates. +Batch-adds addresses to the whitelist. Silently skips duplicates, but **reverts** on `address(0)` (the mint/burn sentinel is never a list member — skipping it would make the emitted event report it as one). ### `removeWhitelistAddress(address targetAddress)` -Removes a single address from the whitelist. Reverts if not listed. +Removes a single address from the whitelist. Reverts with `RuleERC2980_AddressNotWhitelisted` if not listed. ### `removeWhitelistAddresses(address[] calldata targetAddresses)` @@ -84,15 +104,15 @@ ERC-2980 interface getter. Equivalent to `isWhitelisted`. ### `addFrozenlistAddress(address targetAddress)` -Adds a single address to the frozenlist. Reverts if already listed. +Adds a single address to the frozenlist. Reverts with `RuleERC2980_AddressAlreadyFrozen` if already listed. ### `addFrozenlistAddresses(address[] calldata targetAddresses)` -Batch-adds addresses to the frozenlist. Silently skips duplicates. +Batch-adds addresses to the frozenlist. Silently skips duplicates, but **reverts** on `address(0)` (see above). ### `removeFrozenlistAddress(address targetAddress)` -Removes a single address from the frozenlist. Reverts if not listed. +Removes a single address from the frozenlist. Reverts with `RuleERC2980_AddressNotFrozen` if not listed. ### `removeFrozenlistAddresses(address[] calldata targetAddresses)` @@ -122,7 +142,7 @@ Under ERC-2980, the sender is explicitly exempt from the whitelist check. An add ### Deviation from ERC-2980 example interface -The ERC-2980 example `Whitelistable` and `Freezable` interfaces return `false` on duplicate/missing operations. This implementation follows the codebase convention of reverting for single-item operations, while batch operations silently skip invalid entries. +The ERC-2980 example `Whitelistable` and `Freezable` interfaces return `false` on duplicate/missing operations. This implementation follows the codebase convention of reverting for single-item operations, while batch operations silently skip *duplicates*. The zero address is the one input a batch does **not** skip — it reverts, so the emitted event can never report a member that is not in the set. ### `isVerified` semantics diff --git a/doc/technical/RuleIdentityRegistry.md b/doc/technical/RuleIdentityRegistry.md index 1dd07af..8c59fa5 100644 --- a/doc/technical/RuleIdentityRegistry.md +++ b/doc/technical/RuleIdentityRegistry.md @@ -2,7 +2,22 @@ [TOC] -This rule checks an [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) Identity Registry to verify that transfer participants are registered and verified. When an identity registry is configured, the sender, recipient, and spender (in `transferFrom`) are all checked via `isVerified()`. +This rule checks an [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) Identity Registry to verify that transfer participants are registered and verified. + +> ## ✅ ERC-3643 conformant: only the RECEIVER is verified +> +> The specification mandates exactly one identity check: +> +> - *"The **receiver** MUST be whitelisted on the Identity Registry and verified"* (§ Transfer) +> - *"`transferFrom` **works the same way**"* — receiver only +> - *"`mint` and `forcedTransfer` **only require the receiver** to be whitelisted and verified"* +> - *"The `burn` function **bypasses all checks** on eligibility"* +> +> The **sender**, the **spender** and the **minter** are therefore **not** verified by default. +> +> **Why the sender is deliberately not checked:** ERC-3643 screens only the receiver precisely so that an investor whose identity lapses (expired claim, revoked identity) can still **exit their position** by sending to a verified counterparty. Screening the sender would trap them — unable to receive *and* unable to send. +> +> Stricter screening is available as an **explicit opt-in** (`checkSender`, `checkSpender`), never as a silent default. ## Configuration @@ -12,6 +27,8 @@ This rule checks an [ERC-3643](https://eips.ethereum.org/EIPS/eip-3643) Identity | --- | --- | | `admin` | Address granted `DEFAULT_ADMIN_ROLE` (implicitly holds all roles) | | `identityRegistry_` | Address of the identity registry contract (`address(0)` to start without a registry) | +| `checkSender_` | If `true`, ALSO verify the sender. **Stricter than ERC-3643** — pass `false` for the conformant default. Traps de-listed holders (see above). | +| `checkSpender_` | If `true`, ALSO verify the spender on `transferFrom`. **Stricter than ERC-3643** — pass `false` for the conformant default. Mint/burn stay exempt regardless. | ### Behaviour when no registry is set @@ -27,6 +44,14 @@ If no identity registry is configured (`address(0)`), all transfers pass this ru ![surya_inheritance_RuleIdentityRegistry](../surya/surya_inheritance/surya_inheritance_RuleIdentityRegistry.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer, including the ERC-3643 identity registry `isVerified` lookups and the no-registry pass-through case. + +![RuleIdentityRegistry flow with a CMTAT token](../img/rule-identity-registry-flow.png) + +_Diagram source: doc/img/rule-identity-registry-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | @@ -59,11 +84,14 @@ Returns the current identity registry address. Returns `address(0)` if none is s ## Transfer restriction logic - If no registry is set → all transfers pass. -- Burns (`to == address(0)`) always pass, even if the sender is not verified. -- For all other transfers: - - `from` is checked if non-zero (mints where `from == address(0)` skip the sender check). - - `to` is always checked. - - `spender` is checked in `transferFrom` if non-zero. +- **Burns (`to == address(0)`) always pass** — ERC-3643: *"The `burn` function bypasses all checks on eligibility."* +- For all other transfers, including **mint**: + - **`to` is ALWAYS checked.** This is the only check ERC-3643 mandates. + - `from` is checked **only if `checkSender` is enabled** (off by default — stricter than the spec). + - `spender` is checked **only if `checkSpender` is enabled** (off by default — stricter than the spec), and mint + and burn are exempt from it regardless: the minter/burner acts on its own authority, not as a delegated spender. + This is what lets an **unverified minter** mint to a verified recipient, exactly as ERC-3643 requires + (*"`mint` … only require[s] the receiver to be whitelisted and verified"*). ## Usage scenario diff --git a/doc/technical/RuleMaxTotalSupply.md b/doc/technical/RuleMaxTotalSupply.md index 21220e7..0cc5603 100644 --- a/doc/technical/RuleMaxTotalSupply.md +++ b/doc/technical/RuleMaxTotalSupply.md @@ -28,6 +28,14 @@ Both the cap and the token contract address can be updated by the admin after de ![surya_inheritance_RuleMaxTotalSupply](../surya/surya_inheritance/surya_inheritance_RuleMaxTotalSupply.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a mint. Only mints (`from == address(0)`) are gated; transfers and burns pass. + +![RuleMaxTotalSupply flow with a CMTAT token](../img/rule-max-total-supply-flow.png) + +_Diagram source: doc/img/rule-max-total-supply-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | diff --git a/doc/technical/RuleMintAllowance.md b/doc/technical/RuleMintAllowance.md new file mode 100644 index 0000000..76d0cd2 --- /dev/null +++ b/doc/technical/RuleMintAllowance.md @@ -0,0 +1,125 @@ +# Rule Mint Allowance + +[TOC] + +This rule enforces a per-minter mint quota. An operator assigns each minter address a maximum number of tokens it is allowed to mint. Every successful mint reduces the minter's remaining allowance. The operator can adjust the allowance at any time by setting an absolute value or by incrementing/decrementing the current one. + +It is an **operation rule**: it modifies state during the transfer call (deducting from the minter's allowance), unlike validation rules which are read-only. + +Regular transfers and burns are **not restricted** by this rule — it only acts on minting operations (`from == address(0)`). + +`detectTransferRestriction(from, to, value)` (the 3-arg form without a spender) always returns `TRANSFER_OK` because the minter's identity is not available in that call path. Use `detectTransferRestrictionFrom(minter, address(0), to, amount)` to query a minter's allowance. + +> ⚠️ **`canTransfer` and `detectTransferRestriction` are NOT authoritative for this rule.** They are hardcoded to "allowed" (`true` / `TRANSFER_OK`) because the 3-arg signature carries no minter identity, so a pre-flight that gates on them will disagree with enforcement: the mint still reverts with `RuleMintAllowance_AllowanceExceeded` if the quota is insufficient. **For a mint pre-flight, always use the spender-aware view** `canTransferFrom(minter, address(0), to, value)` (or `detectTransferRestrictionFrom(minter, address(0), to, value)`), which returns the real answer. + +> Compatibility warning: this rule does **not** enforce mint allowances for a token that only calls the standard ERC-3643 3-arg compliance functions. It requires the spender-aware RuleEngine/CMTAT v3.3+ path (`detectTransferRestrictionFrom` and `transferred(spender, from, to, value)`) so the minter address is available. + +For that reason, `RuleMintAllowance` does not advertise the full ERC-3643 `ICompliance` interface through ERC-165. It still implements the inherited 3-arg callbacks required by the rule interface, but they are not sufficient to enforce mint quotas. + +## Schema + +### Graph + +![surya_graph_RuleMintAllowance](../surya/surya_graph/surya_graph_RuleMintAllowance.sol.png) + +### Inheritance + +![surya_inheritance_RuleMintAllowance](../surya/surya_inheritance/surya_inheritance_RuleMintAllowance.sol.png) + +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a mint. As an operation rule, it decrements the minter's allowance in the `transferred` callback after balances are updated. + +![RuleMintAllowance flow with a CMTAT token](../img/rule-mint-allowance-flow.png) + +_Diagram source: doc/img/rule-mint-allowance-flow.puml._ + +## Restriction codes + +| Constant | Code | Meaning | +| --- | --- | --- | +| `CODE_MINTER_ALLOWANCE_EXCEEDED` | 70 | Minter's remaining allowance is less than the requested mint amount | + +## Access Control + +| Role | Description | +| --- | --- | +| `DEFAULT_ADMIN_ROLE` | Manages all roles; implicitly holds all roles below | +| `ALLOWANCE_OPERATOR_ROLE` | May set, increase, and decrease per-minter allowances | +| `COMPLIANCE_MANAGER_ROLE` | May bind and unbind the rule to a RuleEngine (`bindToken`, `unbindToken`) | + +The state-modifying `transferred()` functions are restricted to the bound entity only. In the standard CMTAT + RuleEngine deployment, bind the rule to the **RuleEngine address** (not the token address), because the RuleEngine is the direct caller of the rule's `transferred()`. The rule targets exactly one bound entity at a time; attempting to bind a second RuleEngine/token reverts with `RuleMintAllowance_TokenAlreadyBound`. To migrate, call `unbindToken` first. + +## Methods + +### `setMintAllowance(address minter, uint256 amount)` + +Sets the `minter`'s allowance to an absolute `amount`, overwriting any previous value. Restricted to `ALLOWANCE_OPERATOR_ROLE`. Emits `MintAllowanceSet`. + +### `increaseMintAllowance(address minter, uint256 amount)` + +Adds `amount` to `minter`'s current allowance. Restricted to `ALLOWANCE_OPERATOR_ROLE`. Emits `MintAllowanceIncreased`. + +### `decreaseMintAllowance(address minter, uint256 amount)` + +Subtracts `amount` from `minter`'s current allowance. Reverts with `RuleMintAllowance_DecreaseBelowZero` if `amount` exceeds the current allowance. Restricted to `ALLOWANCE_OPERATOR_ROLE`. Emits `MintAllowanceDecreased`. + +### `clearMintAllowances(address[] calldata minters)` + +Sets the allowance of every listed minter to zero. Batch operation: does not revert on minters that already have a zero allowance, on duplicates, or on an empty array. Restricted to `ALLOWANCE_OPERATOR_ROLE`. Emits `MintAllowanceSet(minter, 0)` per entry. + +Intended for migration — see the `bindToken` warning below. + +### `mintAllowance(address minter) → uint256` + +Returns the remaining mint allowance for `minter`. Default is `0`. + +### `bindToken(address token)` / `unbindToken(address token)` + +Binds or unbinds the caller address. Only the bound address is authorised to call `transferred`. In practice, bind the RuleEngine address. Restricted to `COMPLIANCE_MANAGER_ROLE`. A second `bindToken` call reverts until the current binding is removed. + +> ⚠️ **`unbindToken` does not clear `mintAllowance`.** Quotas granted while the previous RuleEngine/token was bound remain in storage and are spendable by the same minters as soon as a new caller is bound. The operator who controls rebinding also controls allowances, so the trust model is preserved — but when migrating, call `clearMintAllowances` **before** rebinding if the old quotas must not carry over. + +## Workflow + +1. Deploy `RuleMintAllowance` (or `RuleMintAllowanceOwnable2Step`). +2. Add the rule to the RuleEngine with `ruleEngine.addRule(ruleMintAllowance)`. +3. Bind the rule to the RuleEngine: `ruleMintAllowance.bindToken(address(ruleEngine))`. +4. Set the CMTAT's RuleEngine: `cmtat.setRuleEngine(ruleEngine)`. +5. For each minter, call `setMintAllowance(minterAddress, quota)`. +6. Minters can now mint tokens up to their assigned quota. + +## Allowance deduction + +When CMTAT v3.3+ calls `ruleEngine.transferred(minter, address(0), recipient, amount)`, the rule receives `transferred(minter, address(0), recipient, amount)` and deducts `amount` from `mintAllowance[minter]`. If `amount > mintAllowance[minter]`, the call reverts with `RuleMintAllowance_AllowanceExceeded`. + +## Multiple minters + +Each minter has an independent allowance within the single bound RuleEngine/token. Multiple minters can share a single `RuleMintAllowance` instance, but that instance intentionally targets only one bound caller at a time so allowance state is not shared across multiple RuleEngines/tokens. + +## Notes + +### 3-arg `transferred` path + +When CMTAT calls `ruleEngine.transferred(from, to, value)` without a spender (CMTAT v3.2 and earlier, a pure ERC-3643 token, or when spender is `address(0)`), the rule receives the 3-arg call and performs **no deduction**. The minter's quota is only consumed via the 4-arg path where the minter's address is passed as `spender`. + +Because of this, `RuleMintAllowance` must not be used as a standalone compliance contract for a pure ERC-3643 token. It is intended for the CMTAT/RuleEngine spender-aware integration path. + +### Burns not restricted + +Burns (`to == address(0)`) are not tracked by this rule. Minters do not recover allowance when tokens are burned. + +### Eligibility views: which one is authoritative + +| View | Signature | Authoritative for mint quota? | +| --- | --- | --- | +| `canTransfer` | `(from, to, value)` | ❌ No — hardcoded `true` (no minter identity) | +| `detectTransferRestriction` | `(from, to, value)` | ❌ No — hardcoded `TRANSFER_OK` | +| `canTransferFrom` | `(minter, address(0), to, value)` | ✅ **Yes** — returns `false` when the quota is insufficient | +| `detectTransferRestrictionFrom` | `(minter, address(0), to, value)` | ✅ **Yes** — returns `70` when the quota is insufficient | + +An integrator that pre-flights a mint with `canTransfer` will see "allowed" even when the mint will revert on the quota. This is intentional: the 3-arg views cannot see the minter. Always pre-flight mints with the spender-aware pair, passing the minter as the spender and `address(0)` as `from`. + +## Usage scenario + +An issuer deploys `RuleMintAllowance` and grants `ALLOWANCE_OPERATOR_ROLE` to a compliance officer. The officer assigns `setMintAllowance(alice, 100_000e18)` — Alice may mint up to 100 000 tokens. Each `cmtat.mint(recipient, amount)` call by Alice reduces her quota. Once exhausted, further mints by Alice revert. The officer can call `increaseMintAllowance(alice, 50_000e18)` to extend Alice's quota or `setMintAllowance(alice, 0)` to revoke it entirely. diff --git a/doc/technical/RuleSanctionList.md b/doc/technical/RuleSanctionList.md index 27c49bc..20f7995 100644 --- a/doc/technical/RuleSanctionList.md +++ b/doc/technical/RuleSanctionList.md @@ -22,6 +22,14 @@ The oracle can be updated with `setSanctionListOracle` or disabled with `clearSa ![surya_inheritance_RuleSanctionsList](../surya/surya_inheritance/surya_inheritance_RuleSanctionsList.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer, including the Chainalysis oracle lookup and the no-oracle pass-through case. + +![RuleSanctionsList flow with a CMTAT token](../img/rule-sanctionslist-flow.png) + +_Diagram source: doc/img/rule-sanctionslist-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | diff --git a/doc/technical/RuleSpenderWhitelist.md b/doc/technical/RuleSpenderWhitelist.md index bf3281f..045969d 100644 --- a/doc/technical/RuleSpenderWhitelist.md +++ b/doc/technical/RuleSpenderWhitelist.md @@ -23,6 +23,14 @@ This rule restricts only spender-initiated transfers (`transferFrom`): the spend ![surya_inheritance_RuleSpenderWhitelist](../surya/surya_inheritance/surya_inheritance_RuleSpenderWhitelist.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer. Direct `transfer` calls always pass; only `transferFrom` spenders are gated. + +![RuleSpenderWhitelist flow with a CMTAT token](../img/rule-spender-whitelist-flow.png) + +_Diagram source: doc/img/rule-spender-whitelist-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | diff --git a/doc/technical/RuleWhitelist.md b/doc/technical/RuleWhitelist.md index 7f0bfd4..ad11702 100644 --- a/doc/technical/RuleWhitelist.md +++ b/doc/technical/RuleWhitelist.md @@ -13,7 +13,7 @@ This rule restricts transfers so that only whitelisted addresses may send and re | `admin` | Address granted `DEFAULT_ADMIN_ROLE` (implicitly holds all roles) | | `forwarderIrrevocable` | ERC-2771 trusted forwarder address for meta-transactions (use `address(0)` to disable) | | `checkSpender_` | If `true`, `transferFrom` spender address is also verified against the whitelist | -| `allowMintBurn` | If `true`, pre-lists `address(0)` at deployment to allow mint/burn flows without a post-deploy `addAddress(address(0))` call | +| `allowMintBurn` | If `true`, sets **both** `allowMint` and `allowBurn`. Mint/burn permission is an explicit flag — the zero address is **never** listed. Adjust independently afterwards with `setAllowMint` / `setAllowBurn`. | ### `checkSpender` flag @@ -29,6 +29,14 @@ When `checkSpender` is `true`, the spender in a `transferFrom` call must also be ![surya_inheritance_RuleWhitelist](../surya/surya_inheritance/surya_inheritance_RuleWhitelist.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the rule participates when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer. + +![RuleWhitelist flow with a CMTAT token](../img/rule-whitelist-flow.png) + +_Diagram source: doc/img/rule-whitelist-flow.puml._ + ## Restriction codes | Constant | Code | Meaning | @@ -36,6 +44,8 @@ When `checkSpender` is `true`, the spender in a `transferFrom` call must also be | `CODE_ADDRESS_FROM_NOT_WHITELISTED` | 21 | Sender is not in the whitelist | | `CODE_ADDRESS_TO_NOT_WHITELISTED` | 22 | Recipient is not in the whitelist | | `CODE_ADDRESS_SPENDER_NOT_WHITELISTED` | 23 | Spender is not in the whitelist (only when `checkSpender` is enabled) | +| `CODE_MINT_NOT_ALLOWED` | 24 | Minting is disabled (`allowMint == false`) | +| `CODE_BURN_NOT_ALLOWED` | 25 | Burning is disabled (`allowBurn == false`) | ## Access Control @@ -80,9 +90,27 @@ Enables or disables spender checks for `transferFrom`. Restricted to `DEFAULT_AD ## Notes -### Zero address +### Zero address — the mint/burn sentinel, never a list member + +The zero address **cannot be added to the whitelist**: `addAddress(address(0))` reverts with +`RuleAddressSet_ZeroAddressNotAllowed`, and a batch containing it reverts too. + +It is the ERC-20 mint/burn sentinel, not a participant. Listing it would make the standardized identity getters +assert falsehoods — `isVerified(address(0))` and `contains(address(0))` would return `true`, contradicting ERC-3643, +which defines `isVerified` as *"is this wallet a valid investor holding the required claims"*. + +Mint and burn are instead governed by explicit flags: + +| Flag | Effect | Blocked code | +| --- | --- | --- | +| `allowMint` | Permits `from == address(0)` | `24` (`CODE_MINT_NOT_ALLOWED`) | +| `allowBurn` | Permits `to == address(0)` | `25` (`CODE_BURN_NOT_ALLOWED`) | + +Both are set together by the `allowMintBurn` constructor parameter and are independently settable afterwards +(`setAllowMint` / `setAllowBurn`), so an issuer can permanently close issuance while keeping redemptions open. -The zero address (`address(0)`) may be added to the whitelist. This is required by CMTAT to allow minting (mints are `transfer(address(0), to, value)`) and burning (`to == address(0)`). You can either set `allowMintBurn=true` in the constructor to pre-list `address(0)`, or add it later with `addAddress(address(0))`. OpenZeppelin prevents actual ERC-20 transfers to or from the zero address, so this does not create a security issue. +The flags gate the **operation only**: a permitted mint still requires a whitelisted **recipient**, and a permitted +burn still requires a whitelisted **sender**. ### Batch vs single operations diff --git a/doc/technical/RuleWhitelistWrapper.md b/doc/technical/RuleWhitelistWrapper.md index dc02b31..e922004 100644 --- a/doc/technical/RuleWhitelistWrapper.md +++ b/doc/technical/RuleWhitelistWrapper.md @@ -20,6 +20,14 @@ Each child rule must implement `IAddressList`. The wrapper iterates through all ![surya_inheritance_RuleWhitelistWrapper](../surya/surya_inheritance/surya_inheritance_RuleWhitelistWrapper.sol.png) +### Flow with a CMTAT token + +The sequence below shows how the wrapper aggregates its child whitelist rules when a CMTAT token (with this rule configured in its RuleEngine) processes a transfer. + +![RuleWhitelistWrapper flow with a CMTAT token](../img/rule-whitelist-wrapper-flow.png) + +_Diagram source: doc/img/rule-whitelist-wrapper-flow.puml._ + ## Configuration ### Constructor parameters @@ -79,11 +87,56 @@ Returns the child rule at the given index. Returns the number of registered child rules. -## Notes +## Gas cost of the child-rule scan + +`_detectTransferRestrictionForTargets` makes **one external `STATICCALL` per child rule**: + +```solidity +for (uint256 i = 0; i < rulesLength; ++i) { + bool[] memory isListed = IAddressList(rule(i)).areAddressesListed(targetAddress); // <- external call + for (uint256 j = 0; j < targetAddress.length; ++j) { + if (isListed[j]) { result[j] = true; } + } + // early exit: stop as soon as EVERY target address has been resolved + ... +} +``` + +**This is not only a `view` cost.** The wrapper's `transferred()` → `_detectTransferRestriction` path runs the same scan during **transfer execution**, so the gas is paid by the *transferring user*, on every transfer, for the life of the token. + +### Measured cost -### Gas cost +The scan is **linear** in the number of children actually scanned, at a marginal cost of **~8.8k gas per child** (`detectTransferRestriction`, 2 target addresses). Measured: -Each transfer check iterates over all registered child rules. The number of child rules should be kept bounded to avoid excessive gas consumption. +| Children | Allowed pair, resolved only by the **last** child | Rejected pair (**never** early-exits) | Marginal gas/child | +| --- | --- | --- | --- | +| 1 | ~7.0k | ~10.7k | — | +| 5 | ~42.3k | ~46.0k | ~8.8k | +| 10 (the default cap) | ~86.4k | ~90.0k | ~9.0k | +| 25 | — | ~222.3k | ~8.9k | +| 50 | — | ~442.8k | ~8.9k | +| 100 | — | ~884.2k | ~8.8k | +| 200 | — | ~1.77M | ~8.8k | + +The marginal cost stays flat (8,891 → 8,855 → 8,841 → 8,841 gas/child from n=25 to n=200), so memory expansion is negligible at any realistic child count and the model `gas ≈ N × 8.8k` holds. + +With `checkSpender = true` (3 target addresses instead of 2), the same 10-child wrapper costs **~116k** (allowed) to **~121k** (rejected). + +**This is a cost problem, not a liveness problem.** Even 200 children (~1.77M gas) is only ~6% of a 30M block; a transfer would not fail to fit until roughly **3,400 children**. Long before that, the wrapper simply makes every transfer economically painful. Treat the numbers above as a per-holder tax, not as a safety cliff. + +### Two things make the worst case the common case + +1. **The early exit only fires once *every* target address is resolved.** A transfer that is going to be **rejected** — because `from`, `to` (or `spender`) is in *no* child list — never resolves, and therefore scans **all N children**. The most expensive path is the failing one, and the user pays for it before the revert. +2. **`checkSpender = true` adds a third address that must also be found** before the loop can break. It materially lowers the early-exit hit rate and pushes more transfers toward the full-N scan (≈ +35% at 10 children, per the table above). + +### Operator guidance + +- **Keep the child list small — stay at or below the default cap of 10.** `addRule` reverts once `rulesCount() >= maxRules`, and `maxRules` defaults to `DEFAULT_MAX_RULES = 10`. At that cap the worst case is ~90k gas of scanning per transfer: significant, but safe. +- **Raising `maxRules` is a decision with a permanent, per-transfer cost for every holder.** `setMaxRules` only rejects `0` — it accepts any other value. A rules manager who raises the cap to 100 makes the worst-case scan cost **~884k gas on every transfer**; at 200 it is ~1.77M. That is a tax on holders, not a broken token — transfers still fit in a block — but it is paid forever and cannot be refunded. Nothing untrusted can trigger this: only `RULES_MANAGEMENT_ROLE` (or the owner) can add child rules or raise the cap. **The size of the child list is the operator's responsibility.** +- **Order children by expected hit rate.** Put the whitelist that resolves the most addresses first, so the early exit fires as early as possible. This is free and materially reduces the average cost. +- **Prefer fewer, larger child lists over many small ones.** The per-child overhead is an external call; the number of addresses inside a child does not affect the scan cost. + +## Notes ### Usage scenario diff --git a/foundry.toml b/foundry.toml index 74a62cc..c3ab006 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,5 +7,10 @@ optimizer = true optimizer_runs = 200 evm_version = 'prague' +[invariant] +runs = 64 +depth = 128 +fail_on_revert = true + [profile.ci] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/CMTAT b/lib/CMTAT index 49544f4..580d477 160000 --- a/lib/CMTAT +++ b/lib/CMTAT @@ -1 +1 @@ -Subproject commit 49544f4de1993008acfc9e848d0bf03bd31d8579 +Subproject commit 580d4776e4cbb857b2da7d83fd79144ae7e47557 diff --git a/lib/RuleEngine b/lib/RuleEngine index ec4a24a..66fcf2a 160000 --- a/lib/RuleEngine +++ b/lib/RuleEngine @@ -1 +1 @@ -Subproject commit ec4a24a96ca30e2ef8f79a06e49846a431e9b4b1 +Subproject commit 66fcf2aafebd1f9d9de8a81dec92b88da071c9b3 diff --git a/script/DeployCMTATWithBlacklist.s.sol b/script/DeployCMTATWithBlacklist.s.sol index 67b819a..5d446bc 100644 --- a/script/DeployCMTATWithBlacklist.s.sol +++ b/script/DeployCMTATWithBlacklist.s.sol @@ -2,13 +2,13 @@ pragma solidity ^0.8.20; import {Script} from "forge-std/Script.sol"; -import {ICMTATConstructor, CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {ICMTATConstructor, CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol"; import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol"; contract DeployCMTATWithBlacklist is Script { - function deploy(address admin, address forwarder) public returns (CMTATStandalone token, RuleBlacklist rule) { + function deploy(address admin, address forwarder) public returns (CMTATStandardStandalone token, RuleBlacklist rule) { ICMTATConstructor.ERC20Attributes memory erc20Attributes = ICMTATConstructor.ERC20Attributes("CMTA Token", "CMTAT", 0); ICMTATConstructor.ExtraInformationAttributes memory extraInformationAttributes = @@ -21,7 +21,7 @@ contract DeployCMTATWithBlacklist is Script { ); ICMTATConstructor.Engine memory engines = ICMTATConstructor.Engine(IRuleEngine(address(0))); - token = new CMTATStandalone(forwarder, address(this), erc20Attributes, extraInformationAttributes, engines); + token = new CMTATStandardStandalone(forwarder, address(this), erc20Attributes, extraInformationAttributes, engines); rule = new RuleBlacklist(admin, address(0)); token.setRuleEngine(IRuleEngine(address(rule))); @@ -32,7 +32,7 @@ contract DeployCMTATWithBlacklist is Script { } } - function run() external returns (CMTATStandalone token, RuleBlacklist rule) { + function run() external returns (CMTATStandardStandalone token, RuleBlacklist rule) { vm.startBroadcast(); (token, rule) = deploy(msg.sender, address(0)); vm.stopBroadcast(); diff --git a/script/DeployCMTATWithBlacklistAndSanctionsList.s.sol b/script/DeployCMTATWithBlacklistAndSanctionsList.s.sol index 6579cf6..3515894 100644 --- a/script/DeployCMTATWithBlacklistAndSanctionsList.s.sol +++ b/script/DeployCMTATWithBlacklistAndSanctionsList.s.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import {Script} from "forge-std/Script.sol"; -import {ICMTATConstructor, CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {ICMTATConstructor, CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol"; import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; @@ -16,7 +16,7 @@ import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol"; * a blacklist (RuleBlacklist) and a sanctions screening (RuleSanctionsList). * * Deployment order: - * 1. CMTATStandalone — token contract (deployer as temporary admin) + * 1. CMTATStandardStandalone — token contract (deployer as temporary admin) * 2. RuleBlacklist — blocks blacklisted senders / recipients * 3. RuleSanctionsList — blocks sanctioned addresses via Chainalysis oracle * 4. RuleEngine — aggregates both rules; token bound at construction @@ -27,7 +27,7 @@ contract DeployCMTATWithBlacklistAndSanctionsList is Script { function deploy(address admin, address forwarder, ISanctionsList sanctionsOracle) public returns ( - CMTATStandalone token, + CMTATStandardStandalone token, RuleEngine ruleEngine, RuleBlacklist ruleBlacklist, RuleSanctionsList ruleSanctionsList @@ -46,7 +46,7 @@ contract DeployCMTATWithBlacklistAndSanctionsList is Script { ICMTATConstructor.Engine memory engines = ICMTATConstructor.Engine(IRuleEngine(address(0))); // Deploy CMTAT with the deployer as temporary admin so we can configure it. - token = new CMTATStandalone(forwarder, address(this), erc20Attributes, extraInformationAttributes, engines); + token = new CMTATStandardStandalone(forwarder, address(this), erc20Attributes, extraInformationAttributes, engines); // Deploy rules; each rule is owned directly by the intended admin. ruleBlacklist = new RuleBlacklist(admin, address(0)); @@ -75,7 +75,7 @@ contract DeployCMTATWithBlacklistAndSanctionsList is Script { function run() external returns ( - CMTATStandalone token, + CMTATStandardStandalone token, RuleEngine ruleEngine, RuleBlacklist ruleBlacklist, RuleSanctionsList ruleSanctionsList diff --git a/script/DeployCMTATWithWhitelist.s.sol b/script/DeployCMTATWithWhitelist.s.sol index e01f4fd..63fb6cc 100644 --- a/script/DeployCMTATWithWhitelist.s.sol +++ b/script/DeployCMTATWithWhitelist.s.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import {Script} from "forge-std/Script.sol"; -import {ICMTATConstructor, CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {ICMTATConstructor, CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol"; import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; @@ -10,7 +10,7 @@ import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; contract DeployCMTATWithWhitelist is Script { function deploy(address admin, address forwarder, bool checkSpender) public - returns (CMTATStandalone token, RuleWhitelist rule) + returns (CMTATStandardStandalone token, RuleWhitelist rule) { ICMTATConstructor.ERC20Attributes memory erc20Attributes = ICMTATConstructor.ERC20Attributes("CMTA Token", "CMTAT", 0); @@ -24,7 +24,7 @@ contract DeployCMTATWithWhitelist is Script { ); ICMTATConstructor.Engine memory engines = ICMTATConstructor.Engine(IRuleEngine(address(0))); - token = new CMTATStandalone(forwarder, address(this), erc20Attributes, extraInformationAttributes, engines); + token = new CMTATStandardStandalone(forwarder, address(this), erc20Attributes, extraInformationAttributes, engines); rule = new RuleWhitelist(admin, address(0), checkSpender, false); token.setRuleEngine(IRuleEngine(address(rule))); @@ -35,7 +35,7 @@ contract DeployCMTATWithWhitelist is Script { } } - function run() external returns (CMTATStandalone token, RuleWhitelist rule) { + function run() external returns (CMTATStandardStandalone token, RuleWhitelist rule) { vm.startBroadcast(); (token, rule) = deploy(msg.sender, address(0), false); vm.stopBroadcast(); diff --git a/src/mocks/IAddressListInterfaceIdHelper.sol b/src/mocks/IAddressListInterfaceIdHelper.sol new file mode 100644 index 0000000..2861358 --- /dev/null +++ b/src/mocks/IAddressListInterfaceIdHelper.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {IAddressList} from "../rules/interfaces/IAddressList.sol"; +import {IIdentityRegistryContains} from "../rules/interfaces/IIdentityRegistry.sol"; +import {AddressListInterfaceId} from "../rules/interfaces/library/AddressListInterfaceId.sol"; + +/** + * @title IAddressListAllFunctions + * @dev Flattened interface containing ALL functions from the {IAddressList} hierarchy. + * Used to compute the full ERC-165 interface ID (XOR of all selectors). + * + * `type(IAddressList).interfaceId` only covers the functions declared directly on + * `IAddressList` and OMITS `contains(address)`, which is inherited from + * `IIdentityRegistryContains`. `type(IAddressListAllFunctions).interfaceId` covers + * the full hierarchy and is the value stored in {AddressListInterfaceId}. + */ +interface IAddressListAllFunctions { + /* ==== From IAddressList ==== */ + + /** + * @notice Adds several addresses to the set. + * @param targetAddresses The addresses to add. + */ + function addAddresses(address[] calldata targetAddresses) external; + + /** + * @notice Removes several addresses from the set. + * @param targetAddresses The addresses to remove. + */ + function removeAddresses(address[] calldata targetAddresses) external; + + /** + * @notice Adds a single address to the set. + * @param targetAddress The address to add. + */ + function addAddress(address targetAddress) external; + + /** + * @notice Removes a single address from the set. + * @param targetAddress The address to remove. + */ + function removeAddress(address targetAddress) external; + + /** + * @notice Returns the number of addresses currently in the set. + * @return count The number of listed addresses. + */ + function listedAddressCount() external view returns (uint256 count); + + /** + * @notice Returns whether a single address is in the set. + * @param targetAddress The address to check. + * @return isListed True if the address is listed. + */ + function isAddressListed(address targetAddress) external view returns (bool isListed); + + /** + * @notice Returns membership for several addresses in one call. + * @param targetAddresses The addresses to check. + * @return results One boolean per input address, in the same order. + */ + function areAddressesListed(address[] memory targetAddresses) external view returns (bool[] memory results); + + /* ==== From IIdentityRegistryContains ==== */ + + /** + * @notice Returns whether an address is in the set. + * @dev This is the selector that `type(IAddressList).interfaceId` OMITS, because it is + * inherited rather than declared directly. Redeclaring it here is the whole point of + * this flattened interface. + * @param _userAddress The address to check. + * @return True if the address is listed. + */ + function contains(address _userAddress) external view returns (bool); +} + +/** + * @title IAddressListInterfaceIdHelper + * @dev Helper contract exposing the {IAddressList} interface IDs so the pre-computed constant + * can be verified in tests. + */ +contract IAddressListInterfaceIdHelper { + /** + * @notice Returns `type(IAddressList).interfaceId` — INCOMPLETE, omits inherited selectors. + * @return The naive interface ID, which does NOT include the inherited `contains(address)`. + */ + function getIAddressListInterfaceId() external pure returns (bytes4) { + return type(IAddressList).interfaceId; + } + + /** + * @notice Returns the XOR of ALL selectors in the {IAddressList} hierarchy (flattened). + * @return The complete interface ID, including the inherited `contains(address)` selector. + */ + function getIAddressListAllFunctionsInterfaceId() external pure returns (bytes4) { + return type(IAddressListAllFunctions).interfaceId; + } + + /** + * @notice Returns the constant defined in the {AddressListInterfaceId} library. + * @return The pre-computed constant the rules actually advertise via ERC-165. + */ + function getAddressListInterfaceIdConstant() external pure returns (bytes4) { + return AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID; + } + + /** + * @notice Returns the interface ID of the inherited parent interface. + * @return The interface ID of {IIdentityRegistryContains}. + */ + function getIIdentityRegistryContainsInterfaceId() external pure returns (bytes4) { + return type(IIdentityRegistryContains).interfaceId; + } +} diff --git a/src/mocks/IERC3643ComplianceFull.sol b/src/mocks/IERC3643ComplianceFull.sol index 44a1be7..77f43e4 100644 --- a/src/mocks/IERC3643ComplianceFull.sol +++ b/src/mocks/IERC3643ComplianceFull.sol @@ -21,15 +21,56 @@ pragma solidity ^0.8.20; * Computed value: `type(IERC3643ComplianceFull).interfaceId == 0x3144991c` */ interface IERC3643ComplianceFull { - // From IERC3643ComplianceRead - function canTransfer(address from, address to, uint256 value) external view returns (bool isValid); // From IERC3643IComplianceContract + /** + * @notice Notifies the compliance contract that a transfer has occurred. + * @param from The address tokens were transferred from. + * @param to The address tokens were transferred to. + * @param value The amount transferred. + */ function transferred(address from, address to, uint256 value) external; // From IERC3643Compliance (directly defined) + /** + * @notice Binds a token to the compliance contract. + * @param token The token address to bind. + */ function bindToken(address token) external; + /** + * @notice Unbinds a token from the compliance contract. + * @param token The token address to unbind. + */ function unbindToken(address token) external; - function isTokenBound(address token) external view returns (bool isBound); - function getTokenBound() external view returns (address token); + /** + * @notice Notifies the compliance contract that tokens have been created (minted). + * @param to The address that received the newly created tokens. + * @param value The amount created. + */ function created(address to, uint256 value) external; + /** + * @notice Notifies the compliance contract that tokens have been destroyed (burned). + * @param from The address whose tokens were destroyed. + * @param value The amount destroyed. + */ function destroyed(address from, uint256 value) external; + + // From IERC3643ComplianceRead + /** + * @notice Checks whether a transfer is compliant. + * @param from The address tokens would be transferred from. + * @param to The address tokens would be transferred to. + * @param value The amount that would be transferred. + * @return isValid True if the transfer is compliant. + */ + function canTransfer(address from, address to, uint256 value) external view returns (bool isValid); + /** + * @notice Returns whether a token is bound to the compliance contract. + * @param token The token address to query. + * @return isBound True if the token is bound. + */ + function isTokenBound(address token) external view returns (bool isBound); + /** + * @notice Returns the token currently bound to the compliance contract. + * @return token The bound token address. + */ + function getTokenBound() external view returns (address token); } diff --git a/src/mocks/IdentityRegistryMock.sol b/src/mocks/IdentityRegistryMock.sol index 936a26f..d78fddf 100644 --- a/src/mocks/IdentityRegistryMock.sol +++ b/src/mocks/IdentityRegistryMock.sol @@ -1,19 +1,36 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; -import {IIdentityRegistryVerified} from "src/rules/interfaces/IIdentityRegistry.sol"; +import {IIdentityRegistryVerified} from "../rules/interfaces/IIdentityRegistry.sol"; +/** + * @title IdentityRegistryMock — test double for an ERC-3643 identity registry + * @notice Stores per-address verification status for use in tests. + */ contract IdentityRegistryMock is IIdentityRegistryVerified { + /** + * @notice Verification status keyed by address. + */ mapping(address => bool) private verified; /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the verification status of an address. + * @param user The address to update. + * @param verified_ The verification status to assign. + */ function setVerified(address user, bool verified_) external { verified[user] = verified_; } + /** + * @notice Returns whether an address is verified. + * @param user The address to query. + * @return True if the address is verified. + */ function isVerified(address user) external view returns (bool) { return verified[user]; } diff --git a/src/mocks/MockERC20TransferFromFalse.sol b/src/mocks/MockERC20TransferFromFalse.sol index 96469fe..5662812 100644 --- a/src/mocks/MockERC20TransferFromFalse.sol +++ b/src/mocks/MockERC20TransferFromFalse.sol @@ -1,21 +1,45 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; +/** + * @title MockERC20TransferFromFalse — ERC20-like mock whose transferFrom always fails + * @notice Test double that tracks allowances but returns false from transferFrom, + * simulating a token whose transfer silently fails. + */ contract MockERC20TransferFromFalse { + /** + * @notice Allowance amounts keyed by owner then spender. + */ mapping(address => mapping(address => uint256)) private _allowances; /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the allowance granted by an owner to a spender. + * @param owner The address granting the allowance. + * @param spender The address being allowed to spend. + * @param value The allowance amount to set. + */ function setAllowance(address owner, address spender, uint256 value) external { _allowances[owner][spender] = value; } + /** + * @notice Returns the allowance granted by an owner to a spender. + * @param owner The address that granted the allowance. + * @param spender The address allowed to spend. + * @return The current allowance amount. + */ function allowance(address owner, address spender) external view returns (uint256) { return _allowances[owner][spender]; } + /** + * @notice Mock transferFrom that always fails by returning false. + * @return Always false. + */ function transferFrom(address, address, uint256) external pure returns (bool) { return false; } diff --git a/src/mocks/MockERC20WithTransferContext.sol b/src/mocks/MockERC20WithTransferContext.sol index 72ee311..9539ea6 100644 --- a/src/mocks/MockERC20WithTransferContext.sol +++ b/src/mocks/MockERC20WithTransferContext.sol @@ -2,29 +2,59 @@ pragma solidity ^0.8.20; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {ITransferContext} from "src/rules/interfaces/ITransferContext.sol"; +import {ITransferContext} from "../rules/interfaces/ITransferContext.sol"; +/** + * @title MockERC20WithTransferContext — ERC20 mock that notifies a transfer-context rule + * @notice Test double that forwards fungible/multi-token transfer context to a + * configured ITransferContext rule after each transfer. + */ contract MockERC20WithTransferContext is ERC20 { + /** + * @notice The transfer-context rule notified on each transfer. + */ ITransferContext public rule; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the mock ERC20 token. + * @param name_ The token name. + * @param symbol_ The token symbol. + */ constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the transfer-context rule to notify. + * @param rule_ The rule address (set to zero to disable notifications). + */ function setRule(address rule_) external { rule = ITransferContext(rule_); } + /** + * @notice Mints tokens to an address. + * @param to The recipient of the minted tokens. + * @param value The amount to mint. + */ function mint(address to, uint256 value) external { _mint(to, value); } + /** + * @notice Transfers tokens from the caller and notifies the rule with the chosen context. + * @param to The recipient of the transfer. + * @param value The amount to transfer. + * @param useFungibleContext If true, notify with a fungible context; otherwise a multi-token context. + * @param tokenId The token id used when notifying with a multi-token context. + * @return Always true on success. + */ function transferWithContext(address to, uint256 value, bool useFungibleContext, uint256 tokenId) external returns (bool) @@ -38,6 +68,15 @@ contract MockERC20WithTransferContext is ERC20 { return true; } + /** + * @notice Transfers tokens via allowance and notifies the rule with the chosen context. + * @param from The address tokens are transferred from. + * @param to The recipient of the transfer. + * @param value The amount to transfer. + * @param useFungibleContext If true, notify with a fungible context; otherwise a multi-token context. + * @param tokenId The token id used when notifying with a multi-token context. + * @return Always true on success. + */ function transferFromWithContext(address from, address to, uint256 value, bool useFungibleContext, uint256 tokenId) external returns (bool) @@ -58,12 +97,18 @@ contract MockERC20WithTransferContext is ERC20 { PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @inheritdoc ERC20 + */ function transfer(address to, uint256 value) public virtual override returns (bool) { bool success = super.transfer(to, value); _notifyFungible(_msgSender(), _msgSender(), to, value); return success; } + /** + * @inheritdoc ERC20 + */ function transferFrom(address from, address to, uint256 value) public virtual override returns (bool) { address sender = _msgSender(); bool success = super.transferFrom(from, to, value); @@ -75,6 +120,14 @@ contract MockERC20WithTransferContext is ERC20 { INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Builds a fungible transfer context and forwards it to the configured rule. + * @dev No-op when no rule is set. + * @param sender The message sender that initiated the transfer. + * @param from The address tokens were transferred from. + * @param to The address tokens were transferred to. + * @param value The amount transferred. + */ function _notifyFungible(address sender, address from, address to, uint256 value) internal { if (address(rule) == address(0)) { return; @@ -91,6 +144,15 @@ contract MockERC20WithTransferContext is ERC20 { rule.transferred(ctx); } + /** + * @notice Builds a multi-token transfer context and forwards it to the configured rule. + * @dev No-op when no rule is set. + * @param sender The message sender that initiated the transfer. + * @param from The address tokens were transferred from. + * @param to The address tokens were transferred to. + * @param value The amount transferred. + * @param tokenId The token id associated with the transfer. + */ function _notifyMultiToken(address sender, address from, address to, uint256 value, uint256 tokenId) internal { if (address(rule) == address(0)) { return; diff --git a/src/mocks/MockERC721WithTransferContext.sol b/src/mocks/MockERC721WithTransferContext.sol index c6f9879..be93032 100644 --- a/src/mocks/MockERC721WithTransferContext.sol +++ b/src/mocks/MockERC721WithTransferContext.sol @@ -2,25 +2,47 @@ pragma solidity ^0.8.20; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {ITransferContext} from "src/rules/interfaces/ITransferContext.sol"; +import {ITransferContext} from "../rules/interfaces/ITransferContext.sol"; +/** + * @title MockERC721WithTransferContext — ERC721 mock that notifies a transfer-context rule + * @notice Test double that forwards a multi-token transfer context to a configured + * ITransferContext rule after each transferFrom. + */ contract MockERC721WithTransferContext is ERC721 { + /** + * @notice The transfer-context rule notified on each transfer. + */ ITransferContext public rule; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the mock ERC721 token. + * @param name_ The token name. + * @param symbol_ The token symbol. + */ constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the transfer-context rule to notify. + * @param rule_ The rule address (set to zero to disable notifications). + */ function setRule(address rule_) external { rule = ITransferContext(rule_); } + /** + * @notice Mints a token to an address. + * @param to The recipient of the minted token. + * @param tokenId The id of the token to mint. + */ function mint(address to, uint256 tokenId) external { _mint(to, tokenId); } @@ -29,6 +51,9 @@ contract MockERC721WithTransferContext is ERC721 { PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @inheritdoc ERC721 + */ function transferFrom(address from, address to, uint256 tokenId) public virtual override { address sender = _msgSender(); super.transferFrom(from, to, tokenId); @@ -39,6 +64,14 @@ contract MockERC721WithTransferContext is ERC721 { INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Builds a multi-token transfer context and forwards it to the configured rule. + * @dev No-op when no rule is set; value is fixed to 1 for a single NFT. + * @param sender The message sender that initiated the transfer. + * @param from The address the token was transferred from. + * @param to The address the token was transferred to. + * @param tokenId The id of the transferred token. + */ function _notifyRule(address sender, address from, address to, uint256 tokenId) internal { if (address(rule) == address(0)) { return; diff --git a/src/mocks/SanctionListOracle.sol b/src/mocks/SanctionListOracle.sol index 41d586d..65a6894 100644 --- a/src/mocks/SanctionListOracle.sol +++ b/src/mocks/SanctionListOracle.sol @@ -1,23 +1,43 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; -import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol"; +import {ISanctionsList} from "../rules/interfaces/ISanctionsList.sol"; +/** + * @title SanctionListOracle — test double for a Chainalysis-style sanctions oracle + * @notice Stores per-address sanctioned status for use in tests. + */ contract SanctionListOracle is ISanctionsList { + /** + * @notice Sanctioned status keyed by address. + */ mapping(address => bool) private sanctionedAddresses; /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Marks an address as sanctioned. + * @param newSanction The address to sanction. + */ function addToSanctionsList(address newSanction) public { sanctionedAddresses[newSanction] = true; } + /** + * @notice Removes an address from the sanctions list. + * @param removeSanction The address to un-sanction. + */ function removeFromSanctionsList(address removeSanction) public { - sanctionedAddresses[removeSanction] = true; + sanctionedAddresses[removeSanction] = false; } + /** + * @notice Returns whether an address is sanctioned. + * @param addr The address to query. + * @return True if the address is sanctioned. + */ function isSanctioned(address addr) public view returns (bool) { return sanctionedAddresses[addr]; } diff --git a/src/mocks/TotalSupplyMock.sol b/src/mocks/TotalSupplyMock.sol index 3b72179..7fd9f84 100644 --- a/src/mocks/TotalSupplyMock.sol +++ b/src/mocks/TotalSupplyMock.sol @@ -1,17 +1,32 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; +/** + * @title TotalSupplyMock — test double exposing a settable total supply + * @notice Stores a total supply value that tests can set and read back. + */ contract TotalSupplyMock { + /** + * @notice The stored total supply value. + */ uint256 private _totalSupply; /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the total supply value. + * @param newTotalSupply The new total supply to store. + */ function setTotalSupply(uint256 newTotalSupply) external { _totalSupply = newTotalSupply; } + /** + * @notice Returns the stored total supply. + * @return The current total supply value. + */ function totalSupply() external view returns (uint256) { return _totalSupply; } diff --git a/src/mocks/harness/DeploymentCoverageHarnesses.sol b/src/mocks/harness/DeploymentCoverageHarnesses.sol index 3ebbc24..0e307d0 100644 --- a/src/mocks/harness/DeploymentCoverageHarnesses.sol +++ b/src/mocks/harness/DeploymentCoverageHarnesses.sol @@ -1,38 +1,60 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; -import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol"; -import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol"; -import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; -import {RuleWhitelistWrapper} from "src/rules/validation/deployment/RuleWhitelistWrapper.sol"; -import {RuleERC2980} from "src/rules/validation/deployment/RuleERC2980.sol"; -import {RuleSanctionsList} from "src/rules/validation/deployment/RuleSanctionsList.sol"; -import {RuleBlacklistOwnable2Step} from "src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol"; -import {RuleWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol"; -import {RuleWhitelistWrapperOwnable2Step} from "src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol"; -import {RuleERC2980Ownable2Step} from "src/rules/validation/deployment/RuleERC2980Ownable2Step.sol"; - +import {ISanctionsList} from "../../rules/interfaces/ISanctionsList.sol"; +import {RuleBlacklist} from "../../rules/validation/deployment/RuleBlacklist.sol"; +import {RuleWhitelist} from "../../rules/validation/deployment/RuleWhitelist.sol"; +import {RuleWhitelistWrapper} from "../../rules/validation/deployment/RuleWhitelistWrapper.sol"; +import {RuleERC2980} from "../../rules/validation/deployment/RuleERC2980.sol"; +import {RuleSanctionsList} from "../../rules/validation/deployment/RuleSanctionsList.sol"; +import {RuleBlacklistOwnable2Step} from "../../rules/validation/deployment/RuleBlacklistOwnable2Step.sol"; +import {RuleWhitelistOwnable2Step} from "../../rules/validation/deployment/RuleWhitelistOwnable2Step.sol"; +import {RuleWhitelistWrapperOwnable2Step} from "../../rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol"; +import {RuleERC2980Ownable2Step} from "../../rules/validation/deployment/RuleERC2980Ownable2Step.sol"; + +/** + * @title RuleBlacklistHarness — test harness exposing RuleBlacklist internals + */ contract RuleBlacklistHarness is RuleBlacklist { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleBlacklist constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + */ constructor(address admin, address forwarderIrrevocable) RuleBlacklist(admin, forwarderIrrevocable) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleWhitelistHarness — test harness exposing RuleWhitelist internals + */ contract RuleWhitelistHarness is RuleWhitelist { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleWhitelist constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param checkSpender_ Whether the spender is also checked against the whitelist + * @param allowMintBurn Whether mint and burn operations bypass the whitelist + */ constructor(address admin, address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) RuleWhitelist(admin, forwarderIrrevocable, checkSpender_, allowMintBurn) {} @@ -41,34 +63,61 @@ contract RuleWhitelistHarness is RuleWhitelist { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleWhitelistWrapperHarness — test harness exposing RuleWhitelistWrapper internals + */ contract RuleWhitelistWrapperHarness is RuleWhitelistWrapper { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(address admin, address forwarderIrrevocable, bool checkSpender_) - RuleWhitelistWrapper(admin, forwarderIrrevocable, checkSpender_) + /** + * @notice Deploys the harness forwarding to the RuleWhitelistWrapper constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param checkSpender_ Whether the spender is also checked against the whitelist + * @param allowMintBurn Whether minting and burning are permitted (sets both flags) + */ + constructor(address admin, address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) + RuleWhitelistWrapper(admin, forwarderIrrevocable, checkSpender_, allowMintBurn) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleERC2980Harness — test harness exposing RuleERC2980 internals + */ contract RuleERC2980Harness is RuleERC2980 { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleERC2980 constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param allowBurn Whether burn operations are permitted + */ constructor(address admin, address forwarderIrrevocable, bool allowBurn) RuleERC2980(admin, forwarderIrrevocable, allowBurn) {} @@ -77,16 +126,29 @@ contract RuleERC2980Harness is RuleERC2980 { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleSanctionsListHarness — test harness exposing RuleSanctionsList internals + */ contract RuleSanctionsListHarness is RuleSanctionsList { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleSanctionsList constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param sanctionContractOracle_ Chainalysis sanctions oracle used to screen addresses + */ constructor(address admin, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) RuleSanctionsList(admin, forwarderIrrevocable, sanctionContractOracle_) {} @@ -95,32 +157,58 @@ contract RuleSanctionsListHarness is RuleSanctionsList { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleBlacklistOwnable2StepHarness — test harness exposing RuleBlacklistOwnable2Step internals + */ contract RuleBlacklistOwnable2StepHarness is RuleBlacklistOwnable2Step { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleBlacklistOwnable2Step constructor + * @param owner Address set as the contract owner + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + */ constructor(address owner, address forwarderIrrevocable) RuleBlacklistOwnable2Step(owner, forwarderIrrevocable) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleWhitelistOwnable2StepHarness — test harness exposing RuleWhitelistOwnable2Step internals + */ contract RuleWhitelistOwnable2StepHarness is RuleWhitelistOwnable2Step { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleWhitelistOwnable2Step constructor + * @param owner Address set as the contract owner + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param checkSpender_ Whether the spender is also checked against the whitelist + * @param allowMintBurn Whether mint and burn operations bypass the whitelist + */ constructor(address owner, address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) RuleWhitelistOwnable2Step(owner, forwarderIrrevocable, checkSpender_, allowMintBurn) {} @@ -129,34 +217,61 @@ contract RuleWhitelistOwnable2StepHarness is RuleWhitelistOwnable2Step { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleWhitelistWrapperOwnable2StepHarness — test harness exposing RuleWhitelistWrapperOwnable2Step internals + */ contract RuleWhitelistWrapperOwnable2StepHarness is RuleWhitelistWrapperOwnable2Step { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(address owner, address forwarderIrrevocable, bool checkSpender_) - RuleWhitelistWrapperOwnable2Step(owner, forwarderIrrevocable, checkSpender_) + /** + * @notice Deploys the harness forwarding to the RuleWhitelistWrapperOwnable2Step constructor + * @param owner Address set as the contract owner + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param checkSpender_ Whether the spender is also checked against the whitelist + * @param allowMintBurn Whether minting and burning are permitted (sets both flags) + */ + constructor(address owner, address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) + RuleWhitelistWrapperOwnable2Step(owner, forwarderIrrevocable, checkSpender_, allowMintBurn) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } } +/** + * @title RuleERC2980Ownable2StepHarness — test harness exposing RuleERC2980Ownable2Step internals + */ contract RuleERC2980Ownable2StepHarness is RuleERC2980Ownable2Step { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleERC2980Ownable2Step constructor + * @param owner Address set as the contract owner + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param allowBurn Whether burn operations are permitted + */ constructor(address owner, address forwarderIrrevocable, bool allowBurn) RuleERC2980Ownable2Step(owner, forwarderIrrevocable, allowBurn) {} @@ -165,6 +280,10 @@ contract RuleERC2980Ownable2StepHarness is RuleERC2980Ownable2Step { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the length of the internal `_msgData()` calldata buffer + * @return Length in bytes of the value returned by `_msgData()` + */ function exposedMsgDataLength() external view returns (uint256) { return _msgData().length; } diff --git a/src/mocks/harness/RuleSanctionsListOwnable2StepHarness.sol b/src/mocks/harness/RuleSanctionsListOwnable2StepHarness.sol index dedfac7..77da3b4 100644 --- a/src/mocks/harness/RuleSanctionsListOwnable2StepHarness.sol +++ b/src/mocks/harness/RuleSanctionsListOwnable2StepHarness.sol @@ -1,14 +1,23 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; -import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol"; -import {RuleSanctionsListOwnable2Step} from "src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol"; +import {ISanctionsList} from "../../rules/interfaces/ISanctionsList.sol"; +import {RuleSanctionsListOwnable2Step} from "../../rules/validation/deployment/RuleSanctionsListOwnable2Step.sol"; +/** + * @title RuleSanctionsListOwnable2StepHarness — test harness exposing RuleSanctionsListOwnable2Step internals + */ contract RuleSanctionsListOwnable2StepHarness is RuleSanctionsListOwnable2Step { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleSanctionsListOwnable2Step constructor + * @param owner Address set as the contract owner + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param sanctionContractOracle_ Chainalysis sanctions oracle used to screen addresses + */ constructor(address owner, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) RuleSanctionsListOwnable2Step(owner, forwarderIrrevocable, sanctionContractOracle_) {} @@ -17,14 +26,26 @@ contract RuleSanctionsListOwnable2StepHarness is RuleSanctionsListOwnable2Step { EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the internal `_msgSender()` resolved sender + * @return Address returned by `_msgSender()` + */ function exposedMsgSender() external view returns (address) { return _msgSender(); } + /** + * @notice Exposes the internal `_msgData()` calldata buffer + * @return Calldata bytes returned by `_msgData()` + */ function exposedMsgData() external view returns (bytes memory) { return _msgData(); } + /** + * @notice Exposes the internal `_contextSuffixLength()` value + * @return Length in bytes of the ERC-2771 context suffix + */ function exposedContextSuffixLength() external view returns (uint256) { return _contextSuffixLength(); } diff --git a/src/mocks/harness/RuleSpenderWhitelistHarnesses.sol b/src/mocks/harness/RuleSpenderWhitelistHarnesses.sol index 9aa9d82..6289331 100644 --- a/src/mocks/harness/RuleSpenderWhitelistHarnesses.sol +++ b/src/mocks/harness/RuleSpenderWhitelistHarnesses.sol @@ -1,38 +1,66 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; -import {RuleSpenderWhitelist} from "src/rules/validation/deployment/RuleSpenderWhitelist.sol"; -import {RuleSpenderWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol"; +import {RuleSpenderWhitelist} from "../../rules/validation/deployment/RuleSpenderWhitelist.sol"; +import {RuleSpenderWhitelistOwnable2Step} from "../../rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol"; +/** + * @title RuleSpenderWhitelistHarness — test harness exposing RuleSpenderWhitelist internals + */ contract RuleSpenderWhitelistHarness is RuleSpenderWhitelist { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleSpenderWhitelist constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + */ constructor(address admin, address forwarderIrrevocable) RuleSpenderWhitelist(admin, forwarderIrrevocable) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the internal `_msgSender()` resolved sender + * @return Address returned by `_msgSender()` + */ function exposedMsgSender() external view returns (address) { return _msgSender(); } + /** + * @notice Exposes the internal `_msgData()` calldata buffer + * @return Calldata bytes returned by `_msgData()` + */ function exposedMsgData() external view returns (bytes memory) { return _msgData(); } + /** + * @notice Exposes the internal `_contextSuffixLength()` value + * @return Length in bytes of the ERC-2771 context suffix + */ function exposedContextSuffixLength() external view returns (uint256) { return _contextSuffixLength(); } } +/** + * @title RuleSpenderWhitelistOwnable2StepHarness — test harness exposing RuleSpenderWhitelistOwnable2Step internals + */ contract RuleSpenderWhitelistOwnable2StepHarness is RuleSpenderWhitelistOwnable2Step { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the harness forwarding to the RuleSpenderWhitelistOwnable2Step constructor + * @param owner Address set as the contract owner + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + */ constructor(address owner, address forwarderIrrevocable) RuleSpenderWhitelistOwnable2Step(owner, forwarderIrrevocable) {} @@ -41,14 +69,26 @@ contract RuleSpenderWhitelistOwnable2StepHarness is RuleSpenderWhitelistOwnable2 EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the internal `_msgSender()` resolved sender + * @return Address returned by `_msgSender()` + */ function exposedMsgSender() external view returns (address) { return _msgSender(); } + /** + * @notice Exposes the internal `_msgData()` calldata buffer + * @return Calldata bytes returned by `_msgData()` + */ function exposedMsgData() external view returns (bytes memory) { return _msgData(); } + /** + * @notice Exposes the internal `_contextSuffixLength()` value + * @return Length in bytes of the ERC-2771 context suffix + */ function exposedContextSuffixLength() external view returns (uint256) { return _contextSuffixLength(); } diff --git a/src/mocks/harness/RuleWhitelistWrapperHarnessInternal.sol b/src/mocks/harness/RuleWhitelistWrapperHarnessInternal.sol index c02598b..dccd4c4 100644 --- a/src/mocks/harness/RuleWhitelistWrapperHarnessInternal.sol +++ b/src/mocks/harness/RuleWhitelistWrapperHarnessInternal.sol @@ -1,21 +1,38 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; -import {RuleWhitelistWrapper} from "src/rules/validation/deployment/RuleWhitelistWrapper.sol"; +import {RuleWhitelistWrapper} from "../../rules/validation/deployment/RuleWhitelistWrapper.sol"; +/** + * @title RuleWhitelistWrapperHarnessInternal — test harness exposing RuleWhitelistWrapper internal transfer hook + */ contract RuleWhitelistWrapperHarnessInternal is RuleWhitelistWrapper { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(address admin, address forwarderIrrevocable, bool checkSpender_) - RuleWhitelistWrapper(admin, forwarderIrrevocable, checkSpender_) + /** + * @notice Deploys the harness forwarding to the RuleWhitelistWrapper constructor + * @param admin Address granted the admin role + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address + * @param checkSpender_ Whether the spender is also checked against the whitelist + * @param allowMintBurn Whether minting and burning are permitted (sets both flags) + */ + constructor(address admin, address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) + RuleWhitelistWrapper(admin, forwarderIrrevocable, checkSpender_, allowMintBurn) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Exposes the internal `_transferred()` post-transfer hook + * @param spender Address executing the transfer (relevant for `transferFrom`) + * @param from Source address of the transfer + * @param to Destination address of the transfer + * @param value Amount transferred + */ function exposedTransferredSpenderInternal(address spender, address from, address to, uint256 value) external view { _transferred(spender, from, to, value); } diff --git a/src/modules/AccessControlModuleStandalone.sol b/src/modules/AccessControlModuleStandalone.sol index 9c757f4..0fa19db 100644 --- a/src/modules/AccessControlModuleStandalone.sol +++ b/src/modules/AccessControlModuleStandalone.sol @@ -7,6 +7,9 @@ import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; +/** + * @title AccessControlModuleStandalone — base RBAC module where the default admin implicitly holds all roles. + */ abstract contract AccessControlModuleStandalone is AccessControlEnumerable { error AccessControlModuleStandalone_AddressZeroNotAllowed(); @@ -37,7 +40,8 @@ abstract contract AccessControlModuleStandalone is AccessControlEnumerable { //////////////////////////////////////////////////////////////*/ /** - * @dev Returns `true` if `account` has been granted `role`. + * @inheritdoc IAccessControl + * @dev The default admin is treated as holding every role. */ function hasRole(bytes32 role, address account) public diff --git a/src/modules/MetaTxModuleStandalone.sol b/src/modules/MetaTxModuleStandalone.sol index 9902688..12ea2f1 100644 --- a/src/modules/MetaTxModuleStandalone.sol +++ b/src/modules/MetaTxModuleStandalone.sol @@ -8,6 +8,10 @@ import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol" * @dev Meta transaction (gasless) module. */ abstract contract MetaTxModuleStandalone is ERC2771Context { + /** + * @notice Configures the trusted ERC-2771 forwarder used to recover the meta-transaction sender. + * @param trustedForwarder The address allowed to forward meta-transactions on behalf of users. + */ constructor(address trustedForwarder) ERC2771Context(trustedForwarder) { // Nothing to do } diff --git a/src/modules/Ownable2StepERC165Module.sol b/src/modules/Ownable2StepERC165Module.sol new file mode 100644 index 0000000..0c6dcc0 --- /dev/null +++ b/src/modules/Ownable2StepERC165Module.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {OwnableInterfaceId} from "RuleEngine/modules/library/OwnableInterfaceId.sol"; +import {Ownable2StepInterfaceId} from "RuleEngine/modules/library/Ownable2StepInterfaceId.sol"; + +/** + * @title Ownable2StepERC165Module + * @notice Shared ERC-165 advertisement for Ownable2Step deployments. + */ +abstract contract Ownable2StepERC165Module is ERC165 { + /** + * @inheritdoc ERC165 + * @dev Also advertises support for the IERC173 and IOwnable2Step interfaces. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == OwnableInterfaceId.IERC173_INTERFACE_ID + || interfaceId == Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID + || ERC165.supportsInterface(interfaceId); + } +} diff --git a/src/modules/VersionModule.sol b/src/modules/VersionModule.sol index 8da4a78..675192f 100644 --- a/src/modules/VersionModule.sol +++ b/src/modules/VersionModule.sol @@ -8,13 +8,18 @@ import {IERC3643Version} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol * @notice Exposes the contract version as required by ERC-3643. */ abstract contract VersionModule is IERC3643Version { - string private constant VERSION = "0.3.0"; + /** + * @notice The contract version string returned by {version}. + */ + string private constant VERSION = "0.4.0"; /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ - /// @inheritdoc IERC3643Version + /** + * @inheritdoc IERC3643Version + */ function version() public view virtual override returns (string memory version_) { return VERSION; } diff --git a/src/rules/interfaces/IAddressList.sol b/src/rules/interfaces/IAddressList.sol index a2e7173..36e4b79 100644 --- a/src/rules/interfaces/IAddressList.sol +++ b/src/rules/interfaces/IAddressList.sol @@ -3,22 +3,33 @@ pragma solidity ^0.8.20; import {IIdentityRegistryContains} from "./IIdentityRegistry.sol"; +/** + * @title IAddressList — interface for managing and querying a set of addresses. + */ interface IAddressList is IIdentityRegistryContains { /* ============ Events ============ */ - /// @notice Emitted when multiple addresses are added. - /// @param targetAddresses The array of added addresses. + /** + * @notice Emitted when multiple addresses are added. + * @param targetAddresses The array of added addresses. + */ event AddAddresses(address[] targetAddresses); - /// @notice Emitted when multiple addresses are removed. - /// @param targetAddresses The array of removed addresses. + /** + * @notice Emitted when multiple addresses are removed. + * @param targetAddresses The array of removed addresses. + */ event RemoveAddresses(address[] targetAddresses); - /// @notice Emitted when a single address is added. - /// @param targetAddress The added address. + /** + * @notice Emitted when a single address is added. + * @param targetAddress The added address. + */ event AddAddress(address indexed targetAddress); - /// @notice Emitted when a single address is removed. - /// @param targetAddress The removed address. + /** + * @notice Emitted when a single address is removed. + * @param targetAddress The removed address. + */ event RemoveAddress(address indexed targetAddress); /* ============ Write ============ */ diff --git a/src/rules/interfaces/IERC2980.sol b/src/rules/interfaces/IERC2980.sol index adf2f51..f5ca814 100644 --- a/src/rules/interfaces/IERC2980.sol +++ b/src/rules/interfaces/IERC2980.sol @@ -19,12 +19,16 @@ interface IERC2980 { /** * @dev Returns true if the address is in the frozenlist. * Frozen addresses cannot send or receive tokens. + * @param _operator The address to check. + * @return True if the address is in the frozenlist, otherwise false. */ function frozenlist(address _operator) external view returns (bool); /** * @dev Returns true if the address is in the whitelist. * Only whitelisted addresses can receive tokens. + * @param _operator The address to check. + * @return True if the address is in the whitelist, otherwise false. */ function whitelist(address _operator) external view returns (bool); } diff --git a/src/rules/interfaces/IERC7943NonFungibleCompliance.sol b/src/rules/interfaces/IERC7943NonFungibleCompliance.sol index 05cd3f9..fc66f10 100644 --- a/src/rules/interfaces/IERC7943NonFungibleCompliance.sol +++ b/src/rules/interfaces/IERC7943NonFungibleCompliance.sol @@ -42,43 +42,6 @@ interface IERC7943NonFungibleCompliance { * For ERC-721, `amount/value` MUST be set to `1`. */ interface IERC7943NonFungibleComplianceExtend is IERC7943NonFungibleCompliance { - /** - * @notice Returns a transfer-restriction code describing why a transfer is blocked. - * @dev - * - MUST NOT modify state. - * - MUST return `0` when the transfer is allowed. - * - Non-zero codes SHOULD follow the ERC-1404 or RuleEngine restriction-code conventions. - * - * @param from The address currently holding the token. - * @param to The address intended to receive the token. - * @param tokenId The ERC-721/1155 token ID being checked. - * @param amount The amount being transferred (always `1` for ERC-721). - * @return code A restriction code: `0` for success, otherwise an implementation-defined error code. - */ - function detectTransferRestriction(address from, address to, uint256 tokenId, uint256 amount) - external - view - returns (uint8 code); - - /** - * @notice Returns a transfer-restriction code for transfers triggered by a spender. - * @dev - * Similar to `detectTransferRestriction`, but includes the spender performing the transfer. - * - MUST NOT modify state. - * - MUST return `0` when transfer is allowed. - * - * @param spender The caller executing the transfer (owner, operator, or approved address). - * @param from The current owner of the token. - * @param to The intended recipient. - * @param tokenId The token ID being checked. - * @param value The amount being transferred (always `1` for ERC-721). - * @return code A restriction code: `0` for allowed, otherwise a non-zero restriction identifier. - */ - function detectTransferRestrictionFrom(address spender, address from, address to, uint256 tokenId, uint256 value) - external - view - returns (uint8 code); - /** * @notice Determines whether a spender-initiated transfer is allowed. * @dev @@ -124,4 +87,41 @@ interface IERC7943NonFungibleComplianceExtend is IERC7943NonFungibleCompliance { * @param value The transfer amount (always `1` for ERC-721). */ function transferred(address spender, address from, address to, uint256 tokenId, uint256 value) external; + + /** + * @notice Returns a transfer-restriction code describing why a transfer is blocked. + * @dev + * - MUST NOT modify state. + * - MUST return `0` when the transfer is allowed. + * - Non-zero codes SHOULD follow the ERC-1404 or RuleEngine restriction-code conventions. + * + * @param from The address currently holding the token. + * @param to The address intended to receive the token. + * @param tokenId The ERC-721/1155 token ID being checked. + * @param amount The amount being transferred (always `1` for ERC-721). + * @return code A restriction code: `0` for success, otherwise an implementation-defined error code. + */ + function detectTransferRestriction(address from, address to, uint256 tokenId, uint256 amount) + external + view + returns (uint8 code); + + /** + * @notice Returns a transfer-restriction code for transfers triggered by a spender. + * @dev + * Similar to `detectTransferRestriction`, but includes the spender performing the transfer. + * - MUST NOT modify state. + * - MUST return `0` when transfer is allowed. + * + * @param spender The caller executing the transfer (owner, operator, or approved address). + * @param from The current owner of the token. + * @param to The intended recipient. + * @param tokenId The token ID being checked. + * @param value The amount being transferred (always `1` for ERC-721). + * @return code A restriction code: `0` for allowed, otherwise a non-zero restriction identifier. + */ + function detectTransferRestrictionFrom(address spender, address from, address to, uint256 tokenId, uint256 value) + external + view + returns (uint8 code); } diff --git a/src/rules/interfaces/IIdentityRegistry.sol b/src/rules/interfaces/IIdentityRegistry.sol index 86d11d3..6743189 100644 --- a/src/rules/interfaces/IIdentityRegistry.sol +++ b/src/rules/interfaces/IIdentityRegistry.sol @@ -2,12 +2,28 @@ pragma solidity ^0.8.20; +/** + * @title IIdentityRegistryVerified — identity registry verification query. + */ interface IIdentityRegistryVerified { // registry consultation + /** + * @notice Returns whether the given address has a verified identity in the registry. + * @param _userAddress The address to check. + * @return True if the address is verified, otherwise false. + */ function isVerified(address _userAddress) external view returns (bool); } +/** + * @title IIdentityRegistryContains — identity registry membership query. + */ interface IIdentityRegistryContains { // registry consultation + /** + * @notice Returns whether the given address is present in the registry. + * @param _userAddress The address to check. + * @return True if the address is contained in the registry, otherwise false. + */ function contains(address _userAddress) external view returns (bool); } diff --git a/src/rules/interfaces/ISanctionsList.sol b/src/rules/interfaces/ISanctionsList.sol index 8a5775f..312fbc8 100644 --- a/src/rules/interfaces/ISanctionsList.sol +++ b/src/rules/interfaces/ISanctionsList.sol @@ -2,6 +2,14 @@ pragma solidity ^0.8.20; +/** + * @title ISanctionsList — sanctions oracle membership query. + */ interface ISanctionsList { + /** + * @notice Returns whether the given address is sanctioned. + * @param addr The address to check. + * @return True if the address is sanctioned, otherwise false. + */ function isSanctioned(address addr) external view returns (bool); } diff --git a/src/rules/interfaces/ITotalSupply.sol b/src/rules/interfaces/ITotalSupply.sol index ff5fbed..16125b5 100644 --- a/src/rules/interfaces/ITotalSupply.sol +++ b/src/rules/interfaces/ITotalSupply.sol @@ -1,6 +1,13 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; +/** + * @title ITotalSupply — total supply query. + */ interface ITotalSupply { + /** + * @notice Returns the current total supply of the token. + * @return The total supply. + */ function totalSupply() external view returns (uint256); } diff --git a/src/rules/interfaces/ITransferContext.sol b/src/rules/interfaces/ITransferContext.sol index c0fb158..e2e33cf 100644 --- a/src/rules/interfaces/ITransferContext.sol +++ b/src/rules/interfaces/ITransferContext.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; +/** + * @title ITransferContext — transfer context structs and post-transfer hooks. + */ interface ITransferContext { /** * @notice Transfer context for unified rule entrypoints. @@ -41,7 +44,15 @@ interface ITransferContext { bytes data; } + /** + * @notice Notifies the rule of an executed multi-token transfer. + * @param ctx The multi-token transfer context describing the transfer. + */ function transferred(MultiTokenTransferContext calldata ctx) external; + /** + * @notice Notifies the rule of an executed fungible transfer. + * @param ctx The fungible transfer context describing the transfer. + */ function transferred(FungibleTransferContext calldata ctx) external; } diff --git a/src/rules/interfaces/library/AddressListInterfaceId.sol b/src/rules/interfaces/library/AddressListInterfaceId.sol new file mode 100644 index 0000000..24a263c --- /dev/null +++ b/src/rules/interfaces/library/AddressListInterfaceId.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.20; + +/** + * @title AddressListInterfaceId + * @dev ERC-165 interface ID for the full {IAddressList} hierarchy (XOR of all function selectors). + * + * `type(IAddressList).interfaceId` CANNOT be used: it only XORs the selectors declared directly + * on `IAddressList` and omits `contains(address)`, inherited from `IIdentityRegistryContains`. + * This constant is computed from the flattened `IAddressListAllFunctions` interface instead. + * + * Selectors XOR-ed: + * addAddresses(address[]) 0x3628731c + * removeAddresses(address[]) 0xa84eb999 + * addAddress(address) 0x38eada1c + * removeAddress(address) 0x4ba79dfe + * listedAddressCount() 0x2ea5461d + * isAddressListed(address) 0xe3c88c6a + * areAddressesListed(address[]) 0x20e8e17a + * contains(address) 0x5dbe47e8 <- inherited + * + * See src/mocks/IAddressListInterfaceIdHelper.sol; the value is asserted by + * test/InterfaceId/AddressListInterfaceId.t.sol. + */ +library AddressListInterfaceId { + /** + * @notice ERC-165 interface ID of the full {IAddressList} hierarchy. + */ + bytes4 public constant IADDRESS_LIST_INTERFACE_ID = 0x5d10e182; +} diff --git a/src/rules/operation/RuleConditionalTransferLight.sol b/src/rules/operation/RuleConditionalTransferLight.sol index 787427a..bc75ea7 100644 --- a/src/rules/operation/RuleConditionalTransferLight.sol +++ b/src/rules/operation/RuleConditionalTransferLight.sol @@ -10,13 +10,18 @@ import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.s import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol"; import {AccessControlModuleStandalone} from "../../modules/AccessControlModuleStandalone.sol"; import {RuleConditionalTransferLightBase} from "./abstract/RuleConditionalTransferLightBase.sol"; +import {ERC3643ComplianceRolesStorage} from "RuleEngine/modules/library/ERC3643ComplianceRolesStorage.sol"; /** * @title ConditionalTransferLight * @dev Requires operator approval for each transfer. Same transfer (from, to, value) * can be approved multiple times to allow repeated transfers. */ -contract RuleConditionalTransferLight is AccessControlModuleStandalone, RuleConditionalTransferLightBase { +contract RuleConditionalTransferLight is + AccessControlModuleStandalone, + RuleConditionalTransferLightBase, + ERC3643ComplianceRolesStorage +{ /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -30,6 +35,9 @@ contract RuleConditionalTransferLight is AccessControlModuleStandalone, RuleCond PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @inheritdoc IERC165 + */ function supportsInterface(bytes4 interfaceId) public view @@ -39,8 +47,7 @@ contract RuleConditionalTransferLight is AccessControlModuleStandalone, RuleCond { return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID - || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID - || interfaceId == type(IERC7551Compliance).interfaceId + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId || interfaceId == type(IERC3643ComplianceFull).interfaceId || AccessControlEnumerable.supportsInterface(interfaceId); } @@ -49,7 +56,24 @@ contract RuleConditionalTransferLight is AccessControlModuleStandalone, RuleCond ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`. + */ + function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + + /** + * @notice Reverts unless the caller holds `OPERATOR_ROLE`. + */ function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} - function _onlyComplianceManager() internal virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + /** + * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`. + */ + function _authorizeComplianceBindingChange(address) + internal + view + virtual + override + onlyRole(COMPLIANCE_MANAGER_ROLE) + {} } diff --git a/src/rules/operation/RuleConditionalTransferLightMultiToken.sol b/src/rules/operation/RuleConditionalTransferLightMultiToken.sol new file mode 100644 index 0000000..b3bb8e2 --- /dev/null +++ b/src/rules/operation/RuleConditionalTransferLightMultiToken.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol"; +import {AccessControlModuleStandalone} from "../../modules/AccessControlModuleStandalone.sol"; +import {RuleConditionalTransferLightMultiTokenBase} from "./abstract/RuleConditionalTransferLightMultiTokenBase.sol"; +import {ERC3643ComplianceRolesStorage} from "RuleEngine/modules/library/ERC3643ComplianceRolesStorage.sol"; + +/** + * @title RuleConditionalTransferLightMultiToken + * @notice AccessControl variant of the multi-token conditional transfer rule. + * `OPERATOR_ROLE` approves transfers; `COMPLIANCE_MANAGER_ROLE` manages compliance bindings. + */ +contract RuleConditionalTransferLightMultiToken is + AccessControlModuleStandalone, + RuleConditionalTransferLightMultiTokenBase, + ERC3643ComplianceRolesStorage +{ + /** + * @param admin Address of the contract admin. + */ + constructor(address admin) AccessControlModuleStandalone(admin) {} + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(AccessControlEnumerable, IERC165) + returns (bool) + { + return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId + || interfaceId == type(IERC3643ComplianceFull).interfaceId + || AccessControlEnumerable.supportsInterface(interfaceId); + } + + /** + * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`. + */ + function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + + /** + * @notice Reverts unless the caller holds `OPERATOR_ROLE`. + */ + function _authorizeTransferApproval() internal view virtual override onlyRole(OPERATOR_ROLE) {} +} diff --git a/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol b/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol new file mode 100644 index 0000000..cbb4ebf --- /dev/null +++ b/src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol"; +import {RuleConditionalTransferLightMultiTokenBase} from "./abstract/RuleConditionalTransferLightMultiTokenBase.sol"; +import {Ownable2StepERC165Module} from "../../modules/Ownable2StepERC165Module.sol"; + +/** + * @title RuleConditionalTransferLightMultiTokenOwnable2Step + * @notice Ownable2Step variant of the multi-token conditional transfer rule. + * The owner approves transfers and manages compliance bindings. + */ +contract RuleConditionalTransferLightMultiTokenOwnable2Step is + RuleConditionalTransferLightMultiTokenBase, + Ownable2Step, + Ownable2StepERC165Module +{ + /** + * @param owner Address of the contract owner. + */ + constructor(address owner) Ownable(owner) {} + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + override(Ownable2StepERC165Module, IERC165) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId + || interfaceId == type(IERC3643ComplianceFull).interfaceId; + } + + /** + * @notice Reverts unless the caller is the owner. + */ + function _onlyComplianceManager() internal view virtual override onlyOwner {} + + /** + * @notice Reverts unless the caller is the owner. + */ + function _authorizeTransferApproval() internal view virtual override onlyOwner {} +} diff --git a/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol b/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol index 0ba8e65..f5e585e 100644 --- a/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol +++ b/src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol @@ -3,35 +3,50 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; -import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; import {IERC3643ComplianceFull} from "../../mocks/IERC3643ComplianceFull.sol"; import {RuleConditionalTransferLightBase} from "./abstract/RuleConditionalTransferLightBase.sol"; +import {Ownable2StepERC165Module} from "../../modules/Ownable2StepERC165Module.sol"; /** * @title RuleConditionalTransferLightOwnable2Step * @notice Ownable2Step variant of RuleConditionalTransferLight. */ -contract RuleConditionalTransferLightOwnable2Step is RuleConditionalTransferLightBase, Ownable2Step { +contract RuleConditionalTransferLightOwnable2Step is + RuleConditionalTransferLightBase, + Ownable2Step, + Ownable2StepERC165Module +{ /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @param owner Address of the contract owner. + */ constructor(address owner) Ownable(owner) {} /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ - function supportsInterface(bytes4 interfaceId) public view override returns (bool) { - return interfaceId == type(IERC165).interfaceId + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + override(Ownable2StepERC165Module, IERC165) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) || interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID - || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID - || interfaceId == type(IERC7551Compliance).interfaceId + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId || interfaceId == type(IERC3643ComplianceFull).interfaceId; } @@ -39,7 +54,18 @@ contract RuleConditionalTransferLightOwnable2Step is RuleConditionalTransferLigh ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Reverts unless the caller is the owner. + */ + function _onlyComplianceManager() internal view virtual override onlyOwner {} + + /** + * @notice Reverts unless the caller is the owner. + */ function _authorizeTransferApproval() internal view virtual override onlyOwner {} - function _onlyComplianceManager() internal virtual override onlyOwner {} + /** + * @notice Reverts unless the caller is the owner. + */ + function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {} } diff --git a/src/rules/operation/RuleMintAllowance.sol b/src/rules/operation/RuleMintAllowance.sol new file mode 100644 index 0000000..8317383 --- /dev/null +++ b/src/rules/operation/RuleMintAllowance.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {AccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {AccessControlModuleStandalone} from "../../modules/AccessControlModuleStandalone.sol"; +import {RuleMintAllowanceBase} from "./abstract/RuleMintAllowanceBase.sol"; +import {ERC3643ComplianceRolesStorage} from "RuleEngine/modules/library/ERC3643ComplianceRolesStorage.sol"; + +/** + * @title RuleMintAllowance + * @notice AccessControl variant of RuleMintAllowance. + * `DEFAULT_ADMIN_ROLE` implicitly holds all roles. + * `ALLOWANCE_OPERATOR_ROLE` can set, increase, and decrease per-minter allowances. + * `COMPLIANCE_MANAGER_ROLE` can bind/unbind the rule to a RuleEngine. + */ +contract RuleMintAllowance is AccessControlModuleStandalone, RuleMintAllowanceBase, ERC3643ComplianceRolesStorage { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /** + * @param admin Address of the contract admin. + */ + constructor(address admin) AccessControlModuleStandalone(admin) {} + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(AccessControlEnumerable, IERC165) + returns (bool) + { + // Do not advertise full ERC-3643 ICompliance: its 3-arg mint callback + // cannot identify the minter, so quota enforcement needs the spender-aware path. + return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId + || AccessControlEnumerable.supportsInterface(interfaceId); + } + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`. + */ + function _onlyComplianceManager() internal view virtual override onlyRole(COMPLIANCE_MANAGER_ROLE) {} + + /** + * @notice Reverts unless the caller holds `ALLOWANCE_OPERATOR_ROLE`. + */ + function _authorizeSetMintAllowance() internal view virtual override onlyRole(ALLOWANCE_OPERATOR_ROLE) {} + + /** + * @notice Reverts unless the caller holds `COMPLIANCE_MANAGER_ROLE`. + */ + function _authorizeComplianceBindingChange(address) + internal + view + virtual + override + onlyRole(COMPLIANCE_MANAGER_ROLE) + {} +} diff --git a/src/rules/operation/RuleMintAllowanceOwnable2Step.sol b/src/rules/operation/RuleMintAllowanceOwnable2Step.sol new file mode 100644 index 0000000..45470fe --- /dev/null +++ b/src/rules/operation/RuleMintAllowanceOwnable2Step.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {RuleMintAllowanceBase} from "./abstract/RuleMintAllowanceBase.sol"; +import {Ownable2StepERC165Module} from "../../modules/Ownable2StepERC165Module.sol"; + +/** + * @title RuleMintAllowanceOwnable2Step + * @notice Ownable2Step variant of RuleMintAllowance. + * The owner manages all allowances and compliance bindings. + */ +contract RuleMintAllowanceOwnable2Step is RuleMintAllowanceBase, Ownable2Step, Ownable2StepERC165Module { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /** + * @param owner Address of the contract owner. + */ + constructor(address owner) Ownable(owner) {} + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + override(Ownable2StepERC165Module, IERC165) + returns (bool) + { + // Do not advertise full ERC-3643 ICompliance: its 3-arg mint callback + // cannot identify the minter, so quota enforcement needs the spender-aware path. + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID + || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId; + } + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Reverts unless the caller is the owner. + */ + function _onlyComplianceManager() internal view virtual override onlyOwner {} + + /** + * @notice Reverts unless the caller is the owner. + */ + function _authorizeSetMintAllowance() internal view virtual override onlyOwner {} + + /** + * @notice Reverts unless the caller is the owner. + */ + function _authorizeComplianceBindingChange(address) internal view virtual override onlyOwner {} +} diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol b/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol index 83b4b3b..2b781b5 100644 --- a/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol +++ b/src/rules/operation/abstract/RuleConditionalTransferLightApprovalBase.sol @@ -10,7 +10,9 @@ import {RuleConditionalTransferLightInvariantStorage} from "./RuleConditionalTra * No knowledge of token binding or compliance interfaces. */ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTransferLightInvariantStorage { - // Mapping from transfer hash to approval count + /** + * @notice Number of outstanding approvals for each transfer hash (mapping from transfer hash to approval count) + */ mapping(bytes32 => uint256) public approvalCounts; /*////////////////////////////////////////////////////////////// @@ -27,14 +29,14 @@ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTra _; } - function _authorizeTransferApproval() internal view virtual; - - function _authorizeTransferExecution() internal view virtual; - /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Consumes one approval for the transfer described by `ctx`. + * @param ctx The fungible transfer context (from, to, value). + */ function transferred(ITransferContext.FungibleTransferContext calldata ctx) external onlyTransferExecutor { _transferredFromContext(ctx); } @@ -43,12 +45,24 @@ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTra PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Records a new approval for the given transfer, incrementing its approval count. + * @param from The sender of the transfer to approve. + * @param to The recipient of the transfer to approve. + * @param value The amount of the transfer to approve. + */ function approveTransfer(address from, address to, uint256 value) public onlyTransferApprover { bytes32 transferHash = _transferHash(from, to, value); approvalCounts[transferHash] += 1; emit TransferApproved(from, to, value, approvalCounts[transferHash]); } + /** + * @notice Cancels one outstanding approval for the given transfer; reverts if none exists. + * @param from The sender of the transfer whose approval is cancelled. + * @param to The recipient of the transfer whose approval is cancelled. + * @param value The amount of the transfer whose approval is cancelled. + */ function cancelTransferApproval(address from, address to, uint256 value) public onlyTransferApprover { bytes32 transferHash = _transferHash(from, to, value); uint256 count = approvalCounts[transferHash]; @@ -57,6 +71,38 @@ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTra emit TransferApprovalCancelled(from, to, value, approvalCounts[transferHash]); } + /** + * @notice Discards every outstanding approval for the given transfer in one call. + * @dev + * - Reverts if no approval exists, per the single-item convention (use {cancelTransferApproval} + * to remove exactly one). + * - Deliberately does NOT require a bound token: the primary use is cleaning up approvals that + * survived an {unbindToken}, at which point no token is bound. See the {bindToken} warning. + * @param from The sender of the transfer whose approvals are cleared. + * @param to The recipient of the transfer whose approvals are cleared. + * @param value The amount of the transfer whose approvals are cleared. + * @return cleared The approval count that was discarded. + */ + function resetApproval(address from, address to, uint256 value) + public + virtual + onlyTransferApprover + returns (uint256 cleared) + { + bytes32 transferHash = _transferHash(from, to, value); + cleared = approvalCounts[transferHash]; + require(cleared != 0, TransferApprovalNotFound()); + approvalCounts[transferHash] = 0; + emit TransferApprovalReset(from, to, value, cleared); + } + + /** + * @notice Returns the number of outstanding approvals for the given transfer. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + * @return The current approval count for the transfer. + */ function approvedCount(address from, address to, uint256 value) public view returns (uint256) { bytes32 transferHash = _transferHash(from, to, value); return approvalCounts[transferHash]; @@ -66,10 +112,21 @@ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTra INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Consumes one approval for the transfer described by `ctx`. + * @param ctx The fungible transfer context (from, to, value). + */ function _transferredFromContext(ITransferContext.FungibleTransferContext calldata ctx) internal virtual { _transferred(ctx.from, ctx.to, ctx.value); } + /** + * @notice Consumes one approval for the given transfer; reverts if none exists. + * @dev No-op when either endpoint is the zero address (mint/burn). + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + */ function _transferred(address from, address to, uint256 value) internal virtual { if (from == address(0) || to == address(0)) { return; @@ -83,6 +140,13 @@ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTra emit TransferExecuted(from, to, value, approvalCounts[transferHash]); } + /** + * @notice Computes the storage key identifying a (from, to, value) transfer. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + * @return hash The keccak256 hash uniquely identifying the transfer. + */ function _transferHash(address from, address to, uint256 value) internal pure virtual returns (bytes32 hash) { // Linter suggestion (`asm-keccak256`): hash packed values in assembly to avoid abi.encodePacked overhead. assembly ("memory-safe") { @@ -93,4 +157,14 @@ abstract contract RuleConditionalTransferLightApprovalBase is RuleConditionalTra hash := keccak256(ptr, 0x60) } } + + /** + * @notice Authorizes the caller to approve or cancel transfers; reverts if unauthorized. + */ + function _authorizeTransferApproval() internal view virtual; + + /** + * @notice Authorizes the caller to execute (consume) approved transfers; reverts if unauthorized. + */ + function _authorizeTransferExecution() internal view virtual; } diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol b/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol index 7b2bfca..a2cd55f 100644 --- a/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol +++ b/src/rules/operation/abstract/RuleConditionalTransferLightBase.sol @@ -25,14 +25,60 @@ abstract contract RuleConditionalTransferLightBase is { using SafeERC20 for IERC20; + /*////////////////////////////////////////////////////////////// + STATE + //////////////////////////////////////////////////////////////*/ + + /** + * @notice RuleEngine additionally authorized to call the transfer execution hooks. + * @dev Binding is deliberately split into two independent roles: + * + * - {bindToken} — the **ERC-20 token** this rule acts on. It is the token that + * {approveAndTransferIfAllowed} calls `safeTransferFrom` on, and it may + * call `transferred` itself (direct-binding topology). + * - {bindRuleEngine} — a **RuleEngine** allowed to call `transferred` on this rule. Under the + * RuleEngine topology the engine, not the token, is the caller of the + * compliance hooks. + * + * Conflating the two was the bug: with a single slot you had to choose between authorizing + * the engine (which broke {approveAndTransferIfAllowed}, since the engine is not an ERC-20) + * and pointing at the token (which left the engine unauthorized, reverting every transfer). + * Binding both makes the helper usable in either topology. + */ + address public ruleEngine; + /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Compliance hook invoked when tokens are created (minted); consumes an approval if applicable. + * @param to The recipient of the created tokens. + * @param value The amount of tokens created. + */ + function created(address to, uint256 value) external onlyBoundToken { + _transferred(address(0), to, value); + } + + /** + * @notice Compliance hook invoked when tokens are destroyed (burned); consumes an approval if applicable. + * @param from The holder whose tokens are destroyed. + * @param value The amount of tokens destroyed. + */ + function destroyed(address from, uint256 value) external onlyBoundToken { + _transferred(from, address(0), value); + } + + /** + * @inheritdoc IRule + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { return restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED; } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) external pure @@ -45,23 +91,24 @@ abstract contract RuleConditionalTransferLightBase is return TEXT_CODE_NOT_FOUND; } - function created(address to, uint256 value) external onlyBoundToken { - _transferred(address(0), to, value); - } - - function destroyed(address from, uint256 value) external onlyBoundToken { - _transferred(from, address(0), value); - } - /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ /** - * @notice Approves and performs a transferFrom using this rule as spender. + * @notice Approves and performs a transferFrom on the bound ERC-20 token, using this rule as spender. * @dev Requires `from` to have approved this contract on the token. * @dev This function is only safe for tokens that call back `transferred()` during transfer. * @dev CEI is intentionally inverted so the approval exists for the callback. + * @dev Works in BOTH topologies, provided the bindings are set correctly: + * - direct binding: `bindToken(token)` — the token calls back `transferred` itself; + * - RuleEngine: `bindToken(token)` AND `bindRuleEngine(engine)` — the engine calls back. + * The token must be bound with {bindToken}; binding the RuleEngine there instead would make + * `getTokenBound()` a non-ERC-20 and this call would revert. + * @param from The holder to transfer tokens from. + * @param to The recipient of the transfer. + * @param value The amount to transfer. + * @return True when the transfer succeeds. */ function approveAndTransferIfAllowed(address from, address to, uint256 value) public @@ -80,6 +127,9 @@ abstract contract RuleConditionalTransferLightBase is return true; } + /** + * @inheritdoc IERC3643IComplianceContract + */ function transferred(address from, address to, uint256 value) public override(IERC3643IComplianceContract) @@ -88,6 +138,9 @@ abstract contract RuleConditionalTransferLightBase is _transferred(from, to, value); } + /** + * @inheritdoc IRuleEngine + */ function transferred( address, /* spender */ @@ -103,19 +156,85 @@ abstract contract RuleConditionalTransferLightBase is } /** - * @notice Binds a token to this rule. Reverts if a token is already bound. - * @dev Enforces single-token binding to prevent cross-token approval replay. - * To migrate to a new token, call `unbindToken` first. - * @dev WARNING: `unbindToken` does not clear `approvalCounts`. Stale approvals - * from the previous token remain in storage and can be consumed after rebinding. - * The operator who controls rebinding also controls approvals, so the trust - * model is preserved, but integrators should be aware of this behavior. + * @notice Binds the ERC-20 token this rule acts on. Reverts if a token is already bound. + * @dev Only ONE token may be bound at a time. To migrate to a new token, call `unbindToken` first. + * @dev The bound token is BOTH the ERC-20 that {approveAndTransferIfAllowed} transfers, AND an + * authorized caller of `transferred` (the direct-binding topology). If the rule sits behind + * a RuleEngine, additionally call {bindRuleEngine} so the engine may call `transferred` too. + * @dev WARNING: Single-token binding alone does NOT guarantee token-scoped approvals: this rule's + * approvals are keyed `(from, to, value)` with no token dimension. A multi-tenant + * {bindRuleEngine} target would relay several tokens into the same approval bucket — see + * the warning on {bindRuleEngine}. + * @dev WARNING: `unbindToken` does not clear `approvalCounts`, and does not clear the bound + * {ruleEngine} either. Stale approvals from the previous token remain in storage and can be + * consumed after rebinding — and the previously bound engine stays authorized to consume + * them until {unbindRuleEngine} is called. The operator who controls rebinding also controls + * approvals, so the trust model is preserved, but integrators should be aware of this + * behavior. When migrating, call {resetApproval} for each affected transfer AND + * {unbindRuleEngine} before rebinding. + * @param token The ERC-20 token to bind to this rule. */ function bindToken(address token) public override onlyComplianceManager { require(getTokenBound() == address(0), RuleConditionalTransferLight_TokenAlreadyBound()); _bindToken(token); } + /** + * @notice Authorizes a RuleEngine to call this rule's transfer execution hooks. + * @dev Independent of {bindToken}: the engine is authorized to call `transferred`, but is never + * treated as the ERC-20 token. Bind the token with {bindToken} and the engine here, and + * {approveAndTransferIfAllowed} works under the RuleEngine topology. + * Reverts if a RuleEngine is already bound; call {unbindRuleEngine} first to migrate. + * + * @dev WARNING: **Bind ONLY an engine that serves this one token.** + * This rule's approvals are keyed `(from, to, value)` — they carry **no token dimension**. + * A `RuleEngine` is multi-tenant by design (`_boundTokens` is a set), and it relays every + * one of its tokens into the same `transferred(from, to, value)` hook, so the rule cannot + * tell which token moved. If the bound engine serves several tokens, an approval recorded + * for one of them is consumable by ANY of them: + * + * approveTransfer(alice, bob, 100) // intended for token A + * // -> engine -> transferred(alice, bob, 100) + * // the token-A approval is consumed + * + * This is inherent to the single-token rule and is why {RuleConditionalTransferLightMultiToken} + * exists. Binding an engine does not change it — it only makes the topology usable, so the + * constraint must be respected by the operator. If the engine is (or may become) + * multi-tenant, do not use this rule. + * + * @param ruleEngine_ The RuleEngine allowed to call `transferred`. It MUST serve only the token + * bound via {bindToken}. + */ + function bindRuleEngine(address ruleEngine_) public virtual onlyComplianceManager { + require(ruleEngine_ != address(0), RuleConditionalTransferLight_RuleEngineAddressZeroNotAllowed()); + require(ruleEngine == address(0), RuleConditionalTransferLight_RuleEngineAlreadyBound()); + ruleEngine = ruleEngine_; + emit RuleEngineBound(ruleEngine_); + } + + /** + * @notice Revokes the bound RuleEngine's authorization to call the transfer execution hooks. + * @dev Does NOT clear `approvalCounts` — see the {bindToken} warning and {resetApproval}. + */ + function unbindRuleEngine() public virtual onlyComplianceManager { + address previous = ruleEngine; + require(previous != address(0), RuleConditionalTransferLight_RuleEngineNotBound()); + ruleEngine = address(0); + emit RuleEngineUnbound(previous); + } + + /** + * @notice Returns whether `caller` is authorized to call this rule's transfer execution hooks. + * @param caller The address to check. + * @return True if `caller` is the bound token or the bound RuleEngine. + */ + function isTransferExecutor(address caller) public view virtual returns (bool) { + return isTokenBound(caller) || (caller != address(0) && caller == ruleEngine); + } + + /** + * @inheritdoc IERC1404 + */ function detectTransferRestriction(address from, address to, uint256 value) public view @@ -132,6 +251,9 @@ abstract contract RuleConditionalTransferLightBase is return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @inheritdoc IERC1404Extend + */ function detectTransferRestrictionFrom( address, /* spender */ @@ -147,6 +269,9 @@ abstract contract RuleConditionalTransferLightBase is return detectTransferRestriction(from, to, value); } + /** + * @inheritdoc IERC3643ComplianceRead + */ function canTransfer(address from, address to, uint256 value) public view @@ -156,6 +281,9 @@ abstract contract RuleConditionalTransferLightBase is return detectTransferRestriction(from, to, value) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @inheritdoc IERC7551Compliance + */ function canTransferFrom(address spender, address from, address to, uint256 value) public view @@ -170,7 +298,14 @@ abstract contract RuleConditionalTransferLightBase is ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Authorizes transfer execution: the bound token OR the bound RuleEngine may call the + * execution hooks. Both topologies are therefore supported without conflating the two + * roles of the binding — see {ruleEngine}. + */ function _authorizeTransferExecution() internal view override { - require(isTokenBound(_msgSender()), RuleConditionalTransferLight_TransferExecutorUnauthorized(_msgSender())); + require( + isTransferExecutor(_msgSender()), RuleConditionalTransferLight_TransferExecutorUnauthorized(_msgSender()) + ); } } diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol b/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol index 61aed67..3f5126e 100644 --- a/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol +++ b/src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol @@ -3,19 +3,70 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "../../validation/abstract/invariant/RuleSharedInvariantStorage.sol"; +/** + * @title RuleConditionalTransferLightInvariantStorage — constants, events and errors for the conditional-transfer rule + */ abstract contract RuleConditionalTransferLightInvariantStorage is RuleSharedInvariantStorage { /* ============ Role ============ */ + /** + * @notice Role allowed to approve, cancel and execute conditional transfers + */ bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); /* ============ State variables ============ */ + /** + * @notice Human-readable message returned when a transfer has not been approved + */ string constant TEXT_TRANSFER_REQUEST_NOT_APPROVED = "ConditionalTransferLight: The request is not approved"; // It is very important that each rule uses an unique code + /** + * @notice Restriction code returned when a transfer request has not been approved + */ uint8 public constant CODE_TRANSFER_REQUEST_NOT_APPROVED = 46; /* ============ Events ============ */ + /** + * @notice Emitted when a transfer is approved + * @param from The sender of the approved transfer + * @param to The recipient of the approved transfer + * @param value The amount of the approved transfer + * @param count The approval count for this transfer after the approval + */ event TransferApproved(address indexed from, address indexed to, uint256 value, uint256 count); + /** + * @notice Emitted when an approved transfer is executed + * @param from The sender of the executed transfer + * @param to The recipient of the executed transfer + * @param value The amount of the executed transfer + * @param remaining The approval count remaining for this transfer after execution + */ event TransferExecuted(address indexed from, address indexed to, uint256 value, uint256 remaining); + /** + * @notice Emitted when a transfer approval is cancelled + * @param from The sender of the cancelled transfer approval + * @param to The recipient of the cancelled transfer approval + * @param value The amount of the cancelled transfer approval + * @param remaining The approval count remaining for this transfer after cancellation + */ event TransferApprovalCancelled(address indexed from, address indexed to, uint256 value, uint256 remaining); + /** + * @notice Emitted when every outstanding approval for a transfer is cleared at once + * @param from The sender of the cleared transfer approvals + * @param to The recipient of the cleared transfer approvals + * @param value The amount of the cleared transfer approvals + * @param cleared The approval count that was discarded + */ + event TransferApprovalReset(address indexed from, address indexed to, uint256 value, uint256 cleared); + /** + * @notice Emitted when a RuleEngine is authorized to call the transfer execution hooks + * @param ruleEngine The RuleEngine now allowed to call `transferred` + */ + event RuleEngineBound(address indexed ruleEngine); + /** + * @notice Emitted when a RuleEngine's authorization to call the transfer execution hooks is revoked + * @param ruleEngine The RuleEngine no longer allowed to call `transferred` + */ + event RuleEngineUnbound(address indexed ruleEngine); /* ============ Custom error ============ */ error RuleConditionalTransferLight_TransferExecutorUnauthorized(address account); @@ -26,4 +77,7 @@ abstract contract RuleConditionalTransferLightInvariantStorage is RuleSharedInva ); error TransferNotApproved(); error TransferApprovalNotFound(); + error RuleConditionalTransferLight_RuleEngineAddressZeroNotAllowed(); + error RuleConditionalTransferLight_RuleEngineNotBound(); + error RuleConditionalTransferLight_RuleEngineAlreadyBound(); } diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol new file mode 100644 index 0000000..08c42e1 --- /dev/null +++ b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenBase.sol @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; +import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {IRule} from "RuleEngine/interfaces/IRule.sol"; +import {ERC3643ComplianceModule} from "RuleEngine/modules/ERC3643ComplianceModule.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {VersionModule} from "../../../modules/VersionModule.sol"; +import { + RuleConditionalTransferLightMultiTokenInvariantStorage +} from "./RuleConditionalTransferLightMultiTokenInvariantStorage.sol"; +import {ITransferContext} from "../../interfaces/ITransferContext.sol"; + +/** + * @title RuleConditionalTransferLightMultiTokenBase — conditional-transfer rule wiring per-token approval state into the compliance interfaces + */ +abstract contract RuleConditionalTransferLightMultiTokenBase is + VersionModule, + ERC3643ComplianceModule, + RuleConditionalTransferLightMultiTokenInvariantStorage, + IRule +{ + using SafeERC20 for IERC20; + + /** + * @notice Number of outstanding approvals for each per-token transfer hash + */ + mapping(bytes32 => uint256) public approvalCounts; + + modifier onlyTransferApprover() { + _authorizeTransferApproval(); + _; + } + + modifier onlyTransferExecutor() { + _authorizeTransferExecution(); + _; + } + + /** + * @notice Compliance hook invoked when tokens are created (minted); consumes an approval if applicable. + * @param to The recipient of the created tokens. + * @param value The amount of tokens created. + */ + function created(address to, uint256 value) external onlyBoundToken { + _transferred(_msgSender(), address(0), to, value); + } + + /** + * @notice Compliance hook invoked when tokens are destroyed (burned); consumes an approval if applicable. + * @param from The holder whose tokens are destroyed. + * @param value The amount of tokens destroyed. + */ + function destroyed(address from, uint256 value) external onlyBoundToken { + _transferred(_msgSender(), from, address(0), value); + } + + /** + * @notice Consumes one approval for the transfer described by `ctx`, using the caller as the token. + * @param ctx The fungible transfer context (from, to, value). + */ + function transferred(ITransferContext.FungibleTransferContext calldata ctx) external onlyTransferExecutor { + _transferred(_msgSender(), ctx.from, ctx.to, ctx.value); + } + + /** + * @inheritdoc IRule + */ + function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { + return restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED; + } + + /** + * @inheritdoc IERC1404 + */ + function messageForTransferRestriction(uint8 restrictionCode) + external + pure + override(IERC1404) + returns (string memory) + { + if (restrictionCode == CODE_TRANSFER_REQUEST_NOT_APPROVED) { + return TEXT_TRANSFER_REQUEST_NOT_APPROVED; + } + return TEXT_CODE_NOT_FOUND; + } + + /** + * @notice Records a new approval for the given per-token transfer. + * @param token The token the transfer applies to. + * @param from The sender of the transfer to approve. + * @param to The recipient of the transfer to approve. + * @param value The amount of the transfer to approve. + */ + function approveTransfer(address token, address from, address to, uint256 value) public onlyTransferApprover { + _approveTransfer(token, from, to, value); + } + + /** + * @notice Cancels one outstanding approval for the given per-token transfer. + * @param token The token the transfer applies to. + * @param from The sender of the transfer whose approval is cancelled. + * @param to The recipient of the transfer whose approval is cancelled. + * @param value The amount of the transfer whose approval is cancelled. + */ + function cancelTransferApproval(address token, address from, address to, uint256 value) + public + onlyTransferApprover + { + _cancelTransferApproval(token, from, to, value); + } + + /** + * @notice Approves and performs a transferFrom of `token` using this rule as spender. + * @dev Requires `from` to have approved this contract on `token`; the token must be bound. + * @param token The token to transfer. + * @param from The holder to transfer tokens from. + * @param to The recipient of the transfer. + * @param value The amount to transfer. + * @return True when the transfer succeeds. + */ + function approveAndTransferIfAllowed(address token, address from, address to, uint256 value) + public + onlyTransferApprover + returns (bool) + { + require(isTokenBound(token), RuleConditionalTransferLightMultiToken_InvalidToken()); + + _approveTransfer(token, from, to, value); + + uint256 allowed = IERC20(token).allowance(from, address(this)); + require( + allowed >= value, RuleConditionalTransferLightMultiToken_InsufficientAllowance(token, from, allowed, value) + ); + + IERC20(token).safeTransferFrom(from, to, value); + return true; + } + + /** + * @inheritdoc IERC3643IComplianceContract + */ + function transferred(address from, address to, uint256 value) + public + override(IERC3643IComplianceContract) + onlyTransferExecutor + { + _transferred(_msgSender(), from, to, value); + } + + /** + * @inheritdoc IRuleEngine + */ + function transferred( + address, + /* spender */ + address from, + address to, + uint256 value + ) + public + override(IRuleEngine) + onlyTransferExecutor + { + _transferred(_msgSender(), from, to, value); + } + + /** + * @notice Discards every outstanding approval for the given per-token transfer in one call. + * @dev + * - Reverts if no approval exists, per the single-item convention (use {cancelTransferApproval} + * to remove exactly one). + * - Deliberately does NOT require the token to be bound, unlike {approveTransfer}: the primary + * use is cleaning up approvals that survived an {unbindToken}, at which point the token is by + * definition no longer bound. It is also the only way to clear approvals stranded under a key + * that can never be consumed (see `RESULT.md` finding F-4). + * @param token The token whose approvals are cleared. + * @param from The sender of the transfer whose approvals are cleared. + * @param to The recipient of the transfer whose approvals are cleared. + * @param value The amount of the transfer whose approvals are cleared. + * @return cleared The approval count that was discarded. + */ + function resetApproval(address token, address from, address to, uint256 value) + public + virtual + onlyTransferApprover + returns (uint256 cleared) + { + bytes32 transferHash = _transferHash(token, from, to, value); + cleared = approvalCounts[transferHash]; + require(cleared != 0, RuleConditionalTransferLightMultiToken_TransferApprovalNotFound()); + approvalCounts[transferHash] = 0; + emit TransferApprovalReset(token, from, to, value, cleared); + } + + /** + * @notice Returns the number of outstanding approvals for the given per-token transfer. + * @param token The token the transfer applies to. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + * @return The current approval count for the transfer. + */ + function approvedCount(address token, address from, address to, uint256 value) public view returns (uint256) { + bytes32 transferHash = _transferHash(token, from, to, value); + return approvalCounts[transferHash]; + } + + /** + * @inheritdoc IERC1404 + * @dev CALLER-DEPENDENT. The token key is derived from `msg.sender`, so this view only returns a + * meaningful answer when it is invoked BY the bound token. Any other caller — an off-chain + * `eth_call` from a wallet, an explorer, an aggregator — always receives + * `CODE_TRANSFER_REQUEST_NOT_APPROVED`, even for a transfer that is approved and will succeed. + * It is fail-closed, but it carries no signal for third-party pre-flight. + * Use {detectTransferRestrictionForToken} instead, which takes the token explicitly. + */ + function detectTransferRestriction(address from, address to, uint256 value) + public + view + override(IERC1404) + returns (uint8) + { + return _detectTransferRestrictionForToken(_msgSender(), from, to, value); + } + + /** + * @notice Caller-explicit pre-flight: returns the restriction code for a transfer of `token`. + * @dev Unlike {detectTransferRestriction}, the token is passed in rather than derived from + * `msg.sender`, so any caller (notably an off-chain `eth_call`) gets the real answer. + * This is the view integrators should use. + * @param token The token the transfer applies to. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + * @return The restriction code, or TRANSFER_OK when an approval exists. + */ + function detectTransferRestrictionForToken(address token, address from, address to, uint256 value) + public + view + virtual + returns (uint8) + { + return _detectTransferRestrictionForToken(token, from, to, value); + } + + /** + * @notice Caller-explicit pre-flight: whether a transfer of `token` is currently approved. + * @dev The boolean counterpart of {detectTransferRestrictionForToken}. Prefer this over + * {canTransfer}, which is caller-dependent. + * @param token The token the transfer applies to. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + * @return True when the transfer is approved for `token`. + */ + function canTransferForToken(address token, address from, address to, uint256 value) + public + view + virtual + returns (bool) + { + return _detectTransferRestrictionForToken(token, from, to, value) + == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + /** + * @inheritdoc IERC1404Extend + */ + function detectTransferRestrictionFrom( + address, + /* spender */ + address from, + address to, + uint256 value + ) + public + view + override(IERC1404Extend) + returns (uint8) + { + return detectTransferRestriction(from, to, value); + } + + /** + * @inheritdoc IERC3643ComplianceRead + * @dev CALLER-DEPENDENT, for the same reason as {detectTransferRestriction}: a caller that is not + * the bound token always reads `false`. Use {canTransferForToken} for an off-chain pre-flight. + */ + function canTransfer(address from, address to, uint256 value) + public + view + override(IERC3643ComplianceRead) + returns (bool) + { + return detectTransferRestriction(from, to, value) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + /** + * @inheritdoc IERC7551Compliance + */ + function canTransferFrom(address spender, address from, address to, uint256 value) + public + view + override(IERC7551Compliance) + returns (bool) + { + return detectTransferRestrictionFrom(spender, from, to, value) + == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + /** + * @notice Authorizes changes to compliance binding: restricted to the compliance manager. + * @dev NOT `view`, unlike every other access-control hook in this codebase. This is structural, + * not an oversight: the implementation delegates to `_onlyComplianceManager()`, which + * `lib/RuleEngine`'s {ERC3643ComplianceModule} declares as `internal virtual` (non-`view`). + * Solidity checks mutability against a virtual's DECLARED type, not the installed override, + * so calling it from a `view` function is a compile error — even though every override of it + * in this repo is `view`. It can only become `view` once the upstream declaration does. + * (The single-token rules avoid this by overriding this hook directly with `onlyRole(...)` + * instead of delegating, which is why they are already `view`.) + */ + function _authorizeComplianceBindingChange(address) internal virtual override { + _onlyComplianceManager(); + } + + /** + * @notice Records a new approval for the given per-token transfer; reverts if the token is not bound. + * @param token The token the transfer applies to. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + */ + function _approveTransfer(address token, address from, address to, uint256 value) internal virtual { + require(isTokenBound(token), RuleConditionalTransferLightMultiToken_InvalidToken()); + bytes32 transferHash = _transferHash(token, from, to, value); + approvalCounts[transferHash] += 1; + emit TransferApproved(token, from, to, value, approvalCounts[transferHash]); + } + + /** + * @notice Cancels one outstanding approval for the given per-token transfer; reverts if none exists. + * @param token The token the transfer applies to. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + */ + function _cancelTransferApproval(address token, address from, address to, uint256 value) internal virtual { + require(isTokenBound(token), RuleConditionalTransferLightMultiToken_InvalidToken()); + bytes32 transferHash = _transferHash(token, from, to, value); + uint256 count = approvalCounts[transferHash]; + + require(count != 0, RuleConditionalTransferLightMultiToken_TransferApprovalNotFound()); + + approvalCounts[transferHash] = count - 1; + emit TransferApprovalCancelled(token, from, to, value, approvalCounts[transferHash]); + } + + /** + * @notice Consumes one approval for the given per-token transfer; reverts if none exists. + * @dev No-op when either endpoint is the zero address (mint/burn). + * @param token The token the transfer applies to. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + */ + function _transferred(address token, address from, address to, uint256 value) internal virtual { + if (from == address(0) || to == address(0)) { + return; + } + + bytes32 transferHash = _transferHash(token, from, to, value); + uint256 count = approvalCounts[transferHash]; + + require(count != 0, RuleConditionalTransferLightMultiToken_TransferNotApproved()); + + approvalCounts[transferHash] = count - 1; + emit TransferExecuted(token, from, to, value, approvalCounts[transferHash]); + } + + /** + * @notice Computes the restriction code for a transfer of `token`, independently of the caller. + * @dev Single source of truth for the read path: {detectTransferRestriction} feeds it + * `_msgSender()`, while {detectTransferRestrictionForToken} feeds it an explicit token, so + * the two can never disagree. Mints and burns are exempt; an unbound token has no + * consumable approvals and is therefore always restricted (fail-closed). + * @param token The token the transfer applies to. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + * @return The restriction code, or TRANSFER_OK when an approval exists. + */ + function _detectTransferRestrictionForToken(address token, address from, address to, uint256 value) + internal + view + virtual + returns (uint8) + { + if (from == address(0) || to == address(0)) { + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + if (!isTokenBound(token)) { + return CODE_TRANSFER_REQUEST_NOT_APPROVED; + } + + if (approvalCounts[_transferHash(token, from, to, value)] == 0) { + return CODE_TRANSFER_REQUEST_NOT_APPROVED; + } + + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + /** + * @notice Authorizes transfer execution: only a bound token may call the execution hooks. + */ + function _authorizeTransferExecution() internal view virtual { + require( + isTokenBound(_msgSender()), + RuleConditionalTransferLightMultiToken_TransferExecutorUnauthorized(_msgSender()) + ); + } + + /** + * @notice Authorizes the caller to approve or cancel transfers; reverts if unauthorized. + */ + function _authorizeTransferApproval() internal view virtual; + + /** + * @notice Computes the storage key identifying a (token, from, to, value) transfer. + * @param token The token the transfer applies to. + * @param from The sender of the transfer. + * @param to The recipient of the transfer. + * @param value The amount of the transfer. + * @return hash The keccak256 hash uniquely identifying the transfer. + */ + function _transferHash(address token, address from, address to, uint256 value) + internal + pure + virtual + returns (bytes32 hash) + { + assembly ("memory-safe") { + let ptr := mload(0x40) + mstore(ptr, shl(96, token)) + mstore(add(ptr, 0x20), shl(96, from)) + mstore(add(ptr, 0x40), shl(96, to)) + mstore(add(ptr, 0x60), value) + hash := keccak256(ptr, 0x80) + } + } +} diff --git a/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol new file mode 100644 index 0000000..4e69d93 --- /dev/null +++ b/src/rules/operation/abstract/RuleConditionalTransferLightMultiTokenInvariantStorage.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {RuleSharedInvariantStorage} from "../../validation/abstract/invariant/RuleSharedInvariantStorage.sol"; + +/** + * @title RuleConditionalTransferLightMultiTokenInvariantStorage — constants, events and errors for the multi-token conditional-transfer rule + */ +abstract contract RuleConditionalTransferLightMultiTokenInvariantStorage is RuleSharedInvariantStorage { + /* ============ Role ============ */ + /** + * @notice Role allowed to approve, cancel and execute conditional transfers + */ + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + /* ============ State variables ============ */ + /** + * @notice Human-readable message returned when a transfer has not been approved + */ + string constant TEXT_TRANSFER_REQUEST_NOT_APPROVED = + "ConditionalTransferLightMultiToken: The request is not approved"; + /** + * @notice Restriction code returned when a transfer request has not been approved + */ + uint8 public constant CODE_TRANSFER_REQUEST_NOT_APPROVED = 46; + + /* ============ Events ============ */ + /** + * @notice Emitted when a transfer is approved for a given token + * @param token The token the approval applies to + * @param from The sender of the approved transfer + * @param to The recipient of the approved transfer + * @param value The amount of the approved transfer + * @param count The approval count for this transfer after the approval + */ + event TransferApproved( + address indexed token, address indexed from, address indexed to, uint256 value, uint256 count + ); + /** + * @notice Emitted when an approved transfer is executed for a given token + * @param token The token the transfer applies to + * @param from The sender of the executed transfer + * @param to The recipient of the executed transfer + * @param value The amount of the executed transfer + * @param remaining The approval count remaining for this transfer after execution + */ + event TransferExecuted( + address indexed token, address indexed from, address indexed to, uint256 value, uint256 remaining + ); + /** + * @notice Emitted when a transfer approval is cancelled for a given token + * @param token The token the approval applies to + * @param from The sender of the cancelled transfer approval + * @param to The recipient of the cancelled transfer approval + * @param value The amount of the cancelled transfer approval + * @param remaining The approval count remaining for this transfer after cancellation + */ + event TransferApprovalCancelled( + address indexed token, address indexed from, address indexed to, uint256 value, uint256 remaining + ); + /** + * @notice Emitted when every outstanding approval for a per-token transfer is cleared at once + * @param token The token the cleared approvals applied to + * @param from The sender of the cleared transfer approvals + * @param to The recipient of the cleared transfer approvals + * @param value The amount of the cleared transfer approvals + * @param cleared The approval count that was discarded + */ + event TransferApprovalReset( + address indexed token, address indexed from, address indexed to, uint256 value, uint256 cleared + ); + + /* ============ Custom error ============ */ + error RuleConditionalTransferLightMultiToken_TransferExecutorUnauthorized(address account); + error RuleConditionalTransferLightMultiToken_InsufficientAllowance( + address token, address owner, uint256 allowance, uint256 required + ); + error RuleConditionalTransferLightMultiToken_InvalidToken(); + error RuleConditionalTransferLightMultiToken_TransferNotApproved(); + error RuleConditionalTransferLightMultiToken_TransferApprovalNotFound(); +} diff --git a/src/rules/operation/abstract/RuleMintAllowanceBase.sol b/src/rules/operation/abstract/RuleMintAllowanceBase.sol new file mode 100644 index 0000000..fee137a --- /dev/null +++ b/src/rules/operation/abstract/RuleMintAllowanceBase.sol @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; +import {IERC3643ComplianceRead, IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {IRule} from "RuleEngine/interfaces/IRule.sol"; +import {ERC3643ComplianceModule} from "RuleEngine/modules/ERC3643ComplianceModule.sol"; +import {VersionModule} from "../../../modules/VersionModule.sol"; +import {RuleMintAllowanceInvariantStorage} from "./RuleMintAllowanceInvariantStorage.sol"; + +/** + * @title RuleMintAllowanceBase + * @notice Core logic for per-minter mint quota enforcement. + * @dev Operators set the number of tokens each minter address is allowed to mint. + * Each mint reduces the minter's allowance. Allowances can be set to an absolute + * value or adjusted incrementally via `increaseMintAllowance`/`decreaseMintAllowance`. + * + * The rule tracks mints via the 4-arg `transferred(spender, from=0, to, value)` path + * introduced in CMTAT v3.3. The 3-arg `transferred(from=0, to, value)` path has no + * minter identity and performs no deduction. + * + * `detectTransferRestriction(from, to, value)` always returns TRANSFER_OK because + * the minter identity is unavailable in the 3-arg call; use + * `detectTransferRestrictionFrom(minter, address(0), to, amount)` to query allowance. + * + * Callers of the state-modifying `transferred()` functions must be bound via + * `bindToken(ruleEngineAddress)` before minting starts. In a standard CMTAT + + * RuleEngine setup the RuleEngine address is the entity to bind. + */ +abstract contract RuleMintAllowanceBase is + VersionModule, + ERC3643ComplianceModule, + RuleMintAllowanceInvariantStorage, + IRule +{ + /*////////////////////////////////////////////////////////////// + STATE + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Remaining mint allowance for each minter address, in token base units + */ + mapping(address minter => uint256 allowance) public mintAllowance; + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + modifier onlyAllowanceOperator() { + _authorizeSetMintAllowance(); + _; + } + + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Compliance hook invoked when tokens are created (minted); no-op for this rule. + */ + function created(address, uint256) external virtual override onlyBoundToken {} + + /** + * @notice Compliance hook invoked when tokens are destroyed (burned); no-op for this rule. + */ + function destroyed(address, uint256) external virtual override onlyBoundToken {} + + /** + * @inheritdoc IRule + */ + function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { + return restrictionCode == CODE_MINTER_ALLOWANCE_EXCEEDED; + } + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Sets `minter`'s allowance to an absolute `amount`. + * @param minter The minter whose allowance is set. + * @param amount The absolute allowance to assign. + */ + function setMintAllowance(address minter, uint256 amount) public virtual onlyAllowanceOperator { + _setMintAllowance(minter, amount); + } + + /** + * @notice Increases `minter`'s allowance by `amount`. + * @param minter The minter whose allowance is increased. + * @param amount The amount to add to the allowance. + */ + function increaseMintAllowance(address minter, uint256 amount) public virtual onlyAllowanceOperator { + uint256 newAllowance = mintAllowance[minter] + amount; + mintAllowance[minter] = newAllowance; + emit MintAllowanceIncreased(minter, amount, newAllowance); + } + + /** + * @notice Decreases `minter`'s allowance by `amount`. Reverts if the reduction + * would underflow (i.e. `amount > current allowance`). + * @param minter The minter whose allowance is decreased. + * @param amount The amount to subtract from the allowance. + */ + function decreaseMintAllowance(address minter, uint256 amount) public virtual onlyAllowanceOperator { + uint256 current = mintAllowance[minter]; + require(amount <= current, RuleMintAllowance_DecreaseBelowZero(minter, current, amount)); + uint256 newAllowance = current - amount; + mintAllowance[minter] = newAllowance; + emit MintAllowanceDecreased(minter, amount, newAllowance); + } + + /** + * @notice Sets the allowance of every listed minter to zero. + * @dev + * Batch operation: does not revert on minters that already have a zero allowance. + * Intended for migration: `unbindToken` does NOT clear `mintAllowance`, so call this + * before rebinding to a different RuleEngine/token if the previous quotas must not + * carry over. See the {bindToken} warning. + * @param minters The minters whose allowances are cleared. + */ + function clearMintAllowances(address[] calldata minters) public virtual onlyAllowanceOperator { + for (uint256 i = 0; i < minters.length; ++i) { + _setMintAllowance(minters[i], 0); + } + } + + /** + * @notice Binds a caller to this rule. Reverts if a caller is already bound. + * @dev Enforces single-target binding to prevent one allowance state from being + * shared across several RuleEngines/tokens. To migrate, call `unbindToken` + * first, then bind the new caller. + * @dev WARNING: `unbindToken` does not clear `mintAllowance`. Quotas granted while the + * previous RuleEngine/token was bound remain in storage and are spendable by the same + * minters once a new caller is bound. The operator who controls rebinding also controls + * allowances, so the trust model is preserved, but integrators should be aware of this + * behavior. Call {clearMintAllowances} before rebinding to discard the previous quotas. + * @param token The caller (RuleEngine/token) to bind to this rule. + */ + function bindToken(address token) public virtual override onlyComplianceManager { + require(getTokenBound() == address(0), RuleMintAllowance_TokenAlreadyBound()); + _bindToken(token); + } + + /** + * @dev 3-arg path: no minter identity available; performs no deduction. + * Mints always arrive via the 4-arg path in CMTAT v3.3+. + * @param from The sender address (address(0) for mints). + * @param to The recipient address. + * @param value The amount transferred. + */ + function transferred(address from, address to, uint256 value) + public + virtual + override(IERC3643IComplianceContract) + onlyBoundToken + { + _transferred(from, to, value); + } + + /** + * @dev 4-arg path: `spender` is the minter when `from == address(0)`. + * Deducts `value` from `mintAllowance[spender]`; reverts if insufficient. + * @param spender The minter identity when `from == address(0)`. + * @param from The sender address (address(0) for mints). + * @param to The recipient address. + * @param value The amount transferred. + */ + function transferred(address spender, address from, address to, uint256 value) + public + virtual + override(IRuleEngine) + onlyBoundToken + { + _transferredFrom(spender, from, to, value); + } + + /** + * @inheritdoc IERC1404 + */ + function messageForTransferRestriction(uint8 restrictionCode) + public + pure + override(IERC1404) + returns (string memory) + { + if (restrictionCode == CODE_MINTER_ALLOWANCE_EXCEEDED) { + return TEXT_MINTER_ALLOWANCE_EXCEEDED; + } + return TEXT_CODE_NOT_FOUND; + } + + /** + * @dev Always returns TRANSFER_OK: the minter address is not available in the + * 3-arg call. Call `detectTransferRestrictionFrom` to check a minter's quota. + * @return The restriction code, always TRANSFER_OK. + */ + function detectTransferRestriction(address, address, uint256) + public + view + virtual + override(IERC1404) + returns (uint8) + { + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + /** + * @inheritdoc IERC1404Extend + */ + function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + public + view + virtual + override(IERC1404Extend) + returns (uint8) + { + return _detectTransferRestrictionFrom(spender, from, to, value); + } + + /** + * @dev Always returns true: use `canTransferFrom` to check mint allowance. + * @return Always true. + */ + function canTransfer(address, address, uint256) + public + view + virtual + override(IERC3643ComplianceRead) + returns (bool) + { + return true; + } + + /** + * @inheritdoc IERC7551Compliance + */ + function canTransferFrom(address spender, address from, address to, uint256 value) + public + view + virtual + override(IERC7551Compliance) + returns (bool) + { + return detectTransferRestrictionFrom(spender, from, to, value) + == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice 3-arg path helper: regular transfers carry no minter identity and are not tracked. + */ + function _transferred(address, address, uint256) internal virtual { + // 3-arg path: no minter identity; regular transfers are not tracked by this rule. + } + + /** + * @notice Deducts a mint from the minter's allowance on the 4-arg path. + * @dev No-op unless `from == address(0)` (a mint). Reverts if `value` exceeds + * the minter's remaining allowance. + * @param spender The minter identity. + * @param from The sender address (must be address(0) to deduct). + * @param value The amount minted. + */ + function _transferredFrom(address spender, address from, address, uint256 value) internal virtual { + if (from != address(0)) { + return; + } + uint256 current = mintAllowance[spender]; + require(value <= current, RuleMintAllowance_AllowanceExceeded(address(this), spender, current, value)); + uint256 remaining = current - value; + mintAllowance[spender] = remaining; + emit MintAllowanceConsumed(spender, value, remaining); + } + + /** + * @notice Sets a minter's allowance to an absolute value and emits the event. + * @param minter The minter whose allowance is set. + * @param amount The absolute allowance to assign. + */ + function _setMintAllowance(address minter, uint256 amount) internal virtual { + mintAllowance[minter] = amount; + emit MintAllowanceSet(minter, amount); + } + + /** + * @notice Computes whether a prospective mint would exceed the minter's allowance. + * @dev Only mints (`from == address(0)`) are checked; other transfers return TRANSFER_OK. + * @param spender The minter identity. + * @param from The sender address (checked only when address(0)). + * @param value The amount to be minted. + * @return The restriction code (CODE_MINTER_ALLOWANCE_EXCEEDED or TRANSFER_OK). + */ + function _detectTransferRestrictionFrom(address spender, address from, address, uint256 value) + internal + view + virtual + returns (uint8) + { + if (from == address(0) && mintAllowance[spender] < value) { + return CODE_MINTER_ALLOWANCE_EXCEEDED; + } + return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + } + + /** + * @notice Authorizes the caller to set/adjust mint allowances; reverts if unauthorized. + */ + function _authorizeSetMintAllowance() internal view virtual; +} diff --git a/src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol b/src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol new file mode 100644 index 0000000..249a555 --- /dev/null +++ b/src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {RuleSharedInvariantStorage} from "../../validation/abstract/invariant/RuleSharedInvariantStorage.sol"; + +/** + * @title RuleMintAllowanceInvariantStorage — constants, events and errors for the mint-allowance rule + */ +abstract contract RuleMintAllowanceInvariantStorage is RuleSharedInvariantStorage { + /* ============ Role ============ */ + /** + * @notice Role allowed to set, increase and decrease minter allowances + */ + bytes32 public constant ALLOWANCE_OPERATOR_ROLE = keccak256("ALLOWANCE_OPERATOR_ROLE"); + + /* ============ State variables ============ */ + /** + * @notice Human-readable message returned when a minter's allowance is exceeded + */ + string constant TEXT_MINTER_ALLOWANCE_EXCEEDED = "MintAllowance: minter allowance exceeded"; + // It is very important that each rule uses a unique code + /** + * @notice Restriction code returned when a minter's allowance is exceeded + */ + uint8 public constant CODE_MINTER_ALLOWANCE_EXCEEDED = 70; + + /* ============ Events ============ */ + /** + * @notice Emitted when a minter's allowance is set to an absolute value + * @param minter The minter whose allowance was set + * @param newAllowance The allowance after the update + */ + event MintAllowanceSet(address indexed minter, uint256 newAllowance); + /** + * @notice Emitted when a minter's allowance is increased + * @param minter The minter whose allowance was increased + * @param addedAmount The amount added to the allowance + * @param newAllowance The allowance after the increase + */ + event MintAllowanceIncreased(address indexed minter, uint256 addedAmount, uint256 newAllowance); + /** + * @notice Emitted when a minter's allowance is decreased + * @param minter The minter whose allowance was decreased + * @param reducedAmount The amount subtracted from the allowance + * @param newAllowance The allowance after the decrease + */ + event MintAllowanceDecreased(address indexed minter, uint256 reducedAmount, uint256 newAllowance); + /** + * @notice Emitted when a mint consumes part of a minter's allowance + * @param minter The minter whose allowance was consumed + * @param consumed The amount deducted by the mint + * @param remaining The allowance remaining after consumption + */ + event MintAllowanceConsumed(address indexed minter, uint256 consumed, uint256 remaining); + + /* ============ Custom error ============ */ + error RuleMintAllowance_AllowanceExceeded(address rule, address minter, uint256 allowance, uint256 amount); + error RuleMintAllowance_DecreaseBelowZero(address minter, uint256 currentAllowance, uint256 reductionAmount); + error RuleMintAllowance_TokenAlreadyBound(); +} diff --git a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol index a183d2e..c55bdd8 100644 --- a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol +++ b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSet.sol @@ -19,8 +19,8 @@ import {IAddressList} from "../../../interfaces/IAddressList.sol"; abstract contract RuleAddressSet is MetaTxModuleStandalone, - RuleAddressSetInternal, RuleAddressSetInvariantStorage, + RuleAddressSetInternal, IAddressList { /*////////////////////////////////////////////////////////////// @@ -47,10 +47,6 @@ abstract contract RuleAddressSet is _; } - function _authorizeAddressListAdd() internal view virtual; - - function _authorizeAddressListRemove() internal view virtual; - /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -87,6 +83,7 @@ abstract contract RuleAddressSet is * @param targetAddress The address to be added. */ function addAddress(address targetAddress) public onlyAddressListAdd { + require(targetAddress != address(0), RuleAddressSet_ZeroAddressNotAllowed()); require(!_isAddressListed(targetAddress), RuleAddressSet_AddressAlreadyListed()); _addAddress(targetAddress); emit AddAddress(targetAddress); @@ -147,17 +144,33 @@ abstract contract RuleAddressSet is INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ - /// @inheritdoc ERC2771Context + /** + * @notice Authorizes the caller to add addresses to the set; reverts if unauthorized. + */ + function _authorizeAddressListAdd() internal view virtual; + + /** + * @notice Authorizes the caller to remove addresses from the set; reverts if unauthorized. + */ + function _authorizeAddressListRemove() internal view virtual; + + /** + * @inheritdoc ERC2771Context + */ function _msgSender() internal view virtual override(ERC2771Context) returns (address sender) { return ERC2771Context._msgSender(); } - /// @inheritdoc ERC2771Context + /** + * @inheritdoc ERC2771Context + */ function _msgData() internal view virtual override(ERC2771Context) returns (bytes calldata) { return ERC2771Context._msgData(); } - /// @inheritdoc ERC2771Context + /** + * @inheritdoc ERC2771Context + */ function _contextSuffixLength() internal view virtual override(ERC2771Context) returns (uint256) { return ERC2771Context._contextSuffixLength(); } diff --git a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol index 7fae61d..39e0dd7 100644 --- a/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol +++ b/src/rules/validation/abstract/RuleAddressSet/RuleAddressSetInternal.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; /* ==== OpenZeppelin === */ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {RuleAddressSetInvariantStorage} from "./invariantStorage/RuleAddressSetInvariantStorage.sol"; /** * @title Rule Address Set (Internal) @@ -12,14 +13,16 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet * - Designed for internal inheritance and logic composition. * - Batch operations do not revert when individual entries are invalid. */ -abstract contract RuleAddressSetInternal { +abstract contract RuleAddressSetInternal is RuleAddressSetInvariantStorage { using EnumerableSet for EnumerableSet.AddressSet; /*////////////////////////////////////////////////////////////// STATE VARIABLES //////////////////////////////////////////////////////////////*/ - /// @dev Storage for all listed addresses. + /** + * @dev Storage for all listed addresses. + */ EnumerableSet.AddressSet private _listedAddresses; /*////////////////////////////////////////////////////////////// @@ -37,6 +40,13 @@ abstract contract RuleAddressSetInternal { */ function _addAddresses(address[] calldata addressesToAdd) internal returns (uint256 added, uint256 skipped) { for (uint256 i = 0; i < addressesToAdd.length; ++i) { + // The zero address is the mint/burn sentinel, never a participant. It is REJECTED + // rather than skipped: the batch convention skips *duplicates* (an idempotent no-op that + // the emitted event still describes truthfully), but silently dropping address(0) would + // make `AddAddresses` report a member that is not in the set — re-polluting the very + // off-chain view this guard exists to keep clean. Mint/burn is governed by + // allowMint/allowBurn, never by list membership. + require(addressesToAdd[i] != address(0), RuleAddressSet_ZeroAddressNotAllowed()); if (_listedAddresses.add(addressesToAdd[i])) { added += 1; } else { diff --git a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol index d31478e..36c8233 100644 --- a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol +++ b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleAddressSetInvariantStorage.sol @@ -2,15 +2,37 @@ pragma solidity ^0.8.20; +/** + * @title RuleAddressSetInvariantStorage — roles and errors for the address-set rule. + */ abstract contract RuleAddressSetInvariantStorage { /* ============ Role ============ */ + /** + * @notice Role allowed to remove addresses from the set. + */ bytes32 public constant ADDRESS_LIST_REMOVE_ROLE = keccak256("ADDRESS_LIST_REMOVE_ROLE"); + /** + * @notice Role allowed to add addresses to the set. + */ bytes32 public constant ADDRESS_LIST_ADD_ROLE = keccak256("ADDRESS_LIST_ADD_ROLE"); /* ============ Custom errors ============ */ - /// @notice Thrown when trying to add an address that is already listed. + /** + * @notice Thrown when trying to add an address that is already listed. + */ error RuleAddressSet_AddressAlreadyListed(); - /// @notice Thrown when trying to remove an address that is not listed. + /** + * @notice Thrown when trying to remove an address that is not listed. + */ error RuleAddressSet_AddressNotFound(); + + /** + * @notice Thrown when trying to add the zero address to the set. + * @dev The zero address is the ERC-20 mint/burn sentinel, not a participant. Listing it would + * make `isVerified(address(0))` / `contains(address(0))` return `true`, contradicting + * ERC-3643. Mint/burn permission is governed by the explicit `allowMint` / `allowBurn` + * flags instead. + */ + error RuleAddressSet_ZeroAddressNotAllowed(); } diff --git a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol index a356e7b..9b0ca6f 100644 --- a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol +++ b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol @@ -4,16 +4,37 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "../../invariant/RuleSharedInvariantStorage.sol"; +/** + * @title RuleBlacklistInvariantStorage — constants and errors for the blacklist rule. + */ abstract contract RuleBlacklistInvariantStorage is RuleSharedInvariantStorage { /* ============ String message ============ */ + /** + * @notice Restriction message returned when the sender is blacklisted. + */ string constant TEXT_ADDRESS_FROM_IS_BLACKLISTED = "The sender is blacklisted"; + /** + * @notice Restriction message returned when the recipient is blacklisted. + */ string constant TEXT_ADDRESS_TO_IS_BLACKLISTED = "The recipient is blacklisted"; + /** + * @notice Restriction message returned when the spender is blacklisted. + */ string constant TEXT_ADDRESS_SPENDER_IS_BLACKLISTED = "The spender is blacklisted"; /* ============ Code ============ */ // It is very important that each rule uses an unique code + /** + * @notice Restriction code returned when the sender is blacklisted. + */ uint8 public constant CODE_ADDRESS_FROM_IS_BLACKLISTED = 36; + /** + * @notice Restriction code returned when the recipient is blacklisted. + */ uint8 public constant CODE_ADDRESS_TO_IS_BLACKLISTED = 37; + /** + * @notice Restriction code returned when the spender is blacklisted. + */ uint8 public constant CODE_ADDRESS_SPENDER_IS_BLACKLISTED = 38; error RuleBlacklist_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); diff --git a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol index 59fc65f..cee1a14 100644 --- a/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol +++ b/src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol @@ -4,21 +4,71 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "../../invariant/RuleSharedInvariantStorage.sol"; +/** + * @title RuleWhitelistInvariantStorage — constants and events for the whitelist rule. + */ abstract contract RuleWhitelistInvariantStorage is RuleSharedInvariantStorage { /* ============ String message ============ */ + /** + * @notice Restriction message returned when the sender is not whitelisted. + */ string constant TEXT_ADDRESS_FROM_NOT_WHITELISTED = "The sender is not in the whitelist"; + /** + * @notice Restriction message returned when the recipient is not whitelisted. + */ string constant TEXT_ADDRESS_TO_NOT_WHITELISTED = "The recipient is not in the whitelist"; + /** + * @notice Restriction message returned when the spender is not whitelisted. + */ string constant TEXT_ADDRESS_SPENDER_NOT_WHITELISTED = "The spender is not in the whitelist"; + /** + * @notice Restriction message returned when minting is not allowed. + */ + string constant TEXT_MINT_NOT_ALLOWED = "Minting is not allowed"; + /** + * @notice Restriction message returned when burning is not allowed. + */ + string constant TEXT_BURN_NOT_ALLOWED = "Burning is not allowed"; /* ============ Code ============ */ // It is very important that each rule uses an unique code + /** + * @notice Restriction code returned when the sender is not whitelisted. + */ uint8 public constant CODE_ADDRESS_FROM_NOT_WHITELISTED = 21; + /** + * @notice Restriction code returned when the recipient is not whitelisted. + */ uint8 public constant CODE_ADDRESS_TO_NOT_WHITELISTED = 22; + /** + * @notice Restriction code returned when the spender is not whitelisted. + */ uint8 public constant CODE_ADDRESS_SPENDER_NOT_WHITELISTED = 23; + /** + * @notice Restriction code returned when minting is not allowed by this rule. + */ + uint8 public constant CODE_MINT_NOT_ALLOWED = 24; + /** + * @notice Restriction code returned when burning is not allowed by this rule. + */ + uint8 public constant CODE_BURN_NOT_ALLOWED = 25; /* ============ Events ============ */ - /// @dev Emitted when the `checkSpender` flag is updated. + /** + * @notice Emitted when the `checkSpender` flag is updated. + * @param newValue New value of the `checkSpender` flag. + */ event CheckSpenderUpdated(bool newValue); + /** + * @notice Emitted when the `allowMint` flag is updated. + * @param newValue New value of the `allowMint` flag. + */ + event AllowMintUpdated(bool newValue); + /** + * @notice Emitted when the `allowBurn` flag is updated. + * @param newValue New value of the `allowBurn` flag. + */ + event AllowBurnUpdated(bool newValue); error RuleWhitelist_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); error RuleWhitelist_InvalidTransferFrom( diff --git a/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol b/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol index 2f5890a..7677db3 100644 --- a/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol +++ b/src/rules/validation/abstract/RuleERC2980/RuleERC2980Internal.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; /* ==== OpenZeppelin === */ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {RuleERC2980InvariantStorage} from "./invariantStorage/RuleERC2980InvariantStorage.sol"; /** * @title RuleERC2980Internal @@ -13,28 +14,41 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet * - Frozenlist: frozen addresses may neither send nor receive tokens. * - Batch operations do not revert when individual entries are already present or absent. */ -abstract contract RuleERC2980Internal { +abstract contract RuleERC2980Internal is RuleERC2980InvariantStorage { using EnumerableSet for EnumerableSet.AddressSet; /*////////////////////////////////////////////////////////////// STATE VARIABLES //////////////////////////////////////////////////////////////*/ - /// @dev Addresses allowed to receive tokens. + /** + * @dev Addresses allowed to receive tokens. + */ EnumerableSet.AddressSet private _whitelist; - /// @dev Addresses completely blocked from sending and receiving tokens. + /** + * @dev Addresses completely blocked from sending and receiving tokens. + */ EnumerableSet.AddressSet private _frozenlist; /*////////////////////////////////////////////////////////////// WHITELIST — INTERNAL //////////////////////////////////////////////////////////////*/ + /** + * @notice Adds multiple addresses to the whitelist, skipping any already present. + * @param addressesToAdd Addresses to add to the whitelist. + * @return added Number of addresses newly added. + * @return skipped Number of addresses that were already whitelisted. + */ function _addWhitelistAddresses(address[] calldata addressesToAdd) internal returns (uint256 added, uint256 skipped) { for (uint256 i = 0; i < addressesToAdd.length; ++i) { + // The zero address is the mint/burn sentinel, never a participant. REJECTED rather than + // skipped, so the emitted batch event can never report it as a list member. + require(addressesToAdd[i] != address(0), RuleERC2980_ZeroAddressNotAllowed()); if (_whitelist.add(addressesToAdd[i])) { added += 1; } else { @@ -43,6 +57,12 @@ abstract contract RuleERC2980Internal { } } + /** + * @notice Removes multiple addresses from the whitelist, skipping any that are absent. + * @param addressesToRemove Addresses to remove from the whitelist. + * @return removed Number of addresses actually removed. + * @return skipped Number of addresses that were not whitelisted. + */ function _removeWhitelistAddresses(address[] calldata addressesToRemove) internal returns (uint256 removed, uint256 skipped) @@ -56,31 +76,40 @@ abstract contract RuleERC2980Internal { } } + /** + * @notice Adds a single address to the whitelist. + * @param targetAddress Address to add to the whitelist. + */ function _addWhitelistAddress(address targetAddress) internal virtual { _whitelist.add(targetAddress); } + /** + * @notice Removes a single address from the whitelist. + * @param targetAddress Address to remove from the whitelist. + */ function _removeWhitelistAddress(address targetAddress) internal virtual { _whitelist.remove(targetAddress); } - function _isWhitelisted(address targetAddress) internal view virtual returns (bool) { - return _whitelist.contains(targetAddress); - } - - function _whitelistCount() internal view virtual returns (uint256) { - return _whitelist.length(); - } - /*////////////////////////////////////////////////////////////// FROZENLIST — INTERNAL //////////////////////////////////////////////////////////////*/ + /** + * @notice Adds multiple addresses to the frozenlist, skipping any already present. + * @param addressesToAdd Addresses to add to the frozenlist. + * @return added Number of addresses newly added. + * @return skipped Number of addresses that were already frozen. + */ function _addFrozenlistAddresses(address[] calldata addressesToAdd) internal returns (uint256 added, uint256 skipped) { for (uint256 i = 0; i < addressesToAdd.length; ++i) { + // The zero address is the mint/burn sentinel, never a participant. REJECTED rather than + // skipped, so the emitted batch event can never report it as a list member. + require(addressesToAdd[i] != address(0), RuleERC2980_ZeroAddressNotAllowed()); if (_frozenlist.add(addressesToAdd[i])) { added += 1; } else { @@ -89,6 +118,12 @@ abstract contract RuleERC2980Internal { } } + /** + * @notice Removes multiple addresses from the frozenlist, skipping any that are absent. + * @param addressesToRemove Addresses to remove from the frozenlist. + * @return removed Number of addresses actually removed. + * @return skipped Number of addresses that were not frozen. + */ function _removeFrozenlistAddresses(address[] calldata addressesToRemove) internal returns (uint256 removed, uint256 skipped) @@ -102,18 +137,56 @@ abstract contract RuleERC2980Internal { } } + /** + * @notice Adds a single address to the frozenlist. + * @param targetAddress Address to add to the frozenlist. + */ function _addFrozenlistAddress(address targetAddress) internal virtual { _frozenlist.add(targetAddress); } + /** + * @notice Removes a single address from the frozenlist. + * @param targetAddress Address to remove from the frozenlist. + */ function _removeFrozenlistAddress(address targetAddress) internal virtual { _frozenlist.remove(targetAddress); } + /*////////////////////////////////////////////////////////////// + VIEW — INTERNAL + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns whether an address is whitelisted. + * @param targetAddress Address to check. + * @return True if the address is whitelisted. + */ + function _isWhitelisted(address targetAddress) internal view virtual returns (bool) { + return _whitelist.contains(targetAddress); + } + + /** + * @notice Returns the number of whitelisted addresses. + * @return The count of whitelisted addresses. + */ + function _whitelistCount() internal view virtual returns (uint256) { + return _whitelist.length(); + } + + /** + * @notice Returns whether an address is frozen. + * @param targetAddress Address to check. + * @return True if the address is frozen. + */ function _isFrozen(address targetAddress) internal view virtual returns (bool) { return _frozenlist.contains(targetAddress); } + /** + * @notice Returns the number of frozen addresses. + * @return The count of frozen addresses. + */ function _frozenlistCount() internal view virtual returns (uint256) { return _frozenlist.length(); } diff --git a/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol b/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol index 0de56e5..4748eac 100644 --- a/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol +++ b/src/rules/validation/abstract/RuleERC2980/invariantStorage/RuleERC2980InvariantStorage.sol @@ -3,50 +3,161 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "../../invariant/RuleSharedInvariantStorage.sol"; +/** + * @title RuleERC2980InvariantStorage — constants, roles, events and errors for the ERC-2980 rule. + */ abstract contract RuleERC2980InvariantStorage is RuleSharedInvariantStorage { /* ============ String message ============ */ + /** + * @notice Restriction message returned when the sender is frozen. + */ string constant TEXT_ADDRESS_FROM_IS_FROZEN = "The sender address is frozen"; + /** + * @notice Restriction message returned when the recipient is frozen. + */ string constant TEXT_ADDRESS_TO_IS_FROZEN = "The recipient address is frozen"; + /** + * @notice Restriction message returned when the spender is frozen. + */ string constant TEXT_ADDRESS_SPENDER_IS_FROZEN = "The spender address is frozen"; + /** + * @notice Restriction message returned when the recipient is not whitelisted. + */ string constant TEXT_ADDRESS_TO_NOT_WHITELISTED = "The recipient is not in the whitelist"; + /** + * @notice Restriction message returned when minting is not allowed. + */ + string constant TEXT_MINT_NOT_ALLOWED = "Minting is not allowed"; + /** + * @notice Restriction message returned when burning is not allowed. + */ + string constant TEXT_BURN_NOT_ALLOWED = "Burning is not allowed"; /* ============ Code ============ */ // It is very important that each rule uses a unique code + /** + * @notice Restriction code returned when the sender is frozen. + */ uint8 public constant CODE_ADDRESS_FROM_IS_FROZEN = 60; + /** + * @notice Restriction code returned when the recipient is frozen. + */ uint8 public constant CODE_ADDRESS_TO_IS_FROZEN = 61; + /** + * @notice Restriction code returned when the spender is frozen. + */ uint8 public constant CODE_ADDRESS_SPENDER_IS_FROZEN = 62; + /** + * @notice Restriction code returned when the recipient is not whitelisted. + */ uint8 public constant CODE_ADDRESS_TO_NOT_WHITELISTED = 63; + /** + * @notice Restriction code returned when minting is not allowed by this rule. + */ + uint8 public constant CODE_MINT_NOT_ALLOWED = 64; + /** + * @notice Restriction code returned when burning is not allowed by this rule. + */ + uint8 public constant CODE_BURN_NOT_ALLOWED = 65; /* ============ Roles ============ */ + /** + * @notice Role allowed to add addresses to the whitelist. + */ bytes32 public constant WHITELIST_ADD_ROLE = keccak256("WHITELIST_ADD_ROLE"); + /** + * @notice Role allowed to remove addresses from the whitelist. + */ bytes32 public constant WHITELIST_REMOVE_ROLE = keccak256("WHITELIST_REMOVE_ROLE"); + /** + * @notice Role allowed to add addresses to the frozenlist. + */ bytes32 public constant FROZENLIST_ADD_ROLE = keccak256("FROZENLIST_ADD_ROLE"); + /** + * @notice Role allowed to remove addresses from the frozenlist. + */ bytes32 public constant FROZENLIST_REMOVE_ROLE = keccak256("FROZENLIST_REMOVE_ROLE"); /* ============ Events ============ */ - /// @notice Emitted when multiple addresses are added to the whitelist. + /** + * @notice Emitted when multiple addresses are added to the whitelist. + * @param targetAddresses Addresses added to the whitelist. + */ event AddWhitelistAddresses(address[] targetAddresses); - /// @notice Emitted when multiple addresses are removed from the whitelist. + /** + * @notice Emitted when multiple addresses are removed from the whitelist. + * @param targetAddresses Addresses removed from the whitelist. + */ event RemoveWhitelistAddresses(address[] targetAddresses); - /// @notice Emitted when a single address is added to the whitelist. + /** + * @notice Emitted when a single address is added to the whitelist. + * @param targetAddress Address added to the whitelist. + */ event AddWhitelistAddress(address indexed targetAddress); - /// @notice Emitted when a single address is removed from the whitelist. + /** + * @notice Emitted when a single address is removed from the whitelist. + * @param targetAddress Address removed from the whitelist. + */ event RemoveWhitelistAddress(address indexed targetAddress); - /// @notice Emitted when multiple addresses are added to the frozenlist. + /** + * @notice Emitted when multiple addresses are added to the frozenlist. + * @param targetAddresses Addresses added to the frozenlist. + */ event AddFrozenlistAddresses(address[] targetAddresses); - /// @notice Emitted when multiple addresses are removed from the frozenlist. + /** + * @notice Emitted when multiple addresses are removed from the frozenlist. + * @param targetAddresses Addresses removed from the frozenlist. + */ event RemoveFrozenlistAddresses(address[] targetAddresses); - /// @notice Emitted when a single address is added to the frozenlist. + /** + * @notice Emitted when a single address is added to the frozenlist. + * @param targetAddress Address added to the frozenlist. + */ event AddFrozenlistAddress(address indexed targetAddress); - /// @notice Emitted when a single address is removed from the frozenlist. + /** + * @notice Emitted when a single address is removed from the frozenlist. + * @param targetAddress Address removed from the frozenlist. + */ event RemoveFrozenlistAddress(address indexed targetAddress); + /** + * @notice Emitted when the `allowMint` flag is updated. + * @param newValue New value of the `allowMint` flag. + */ + event AllowMintUpdated(bool newValue); + /** + * @notice Emitted when the `allowBurn` flag is updated. + * @param newValue New value of the `allowBurn` flag. + */ + event AllowBurnUpdated(bool newValue); + /* ============ Custom errors ============ */ error RuleERC2980_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); error RuleERC2980_InvalidTransferFrom( address rule, address spender, address from, address to, uint256 value, uint8 code ); - error RuleERC2980_AddressAlreadyListed(); - error RuleERC2980_AddressNotFound(); + /** + * @notice Thrown when trying to add the zero address to the whitelist or the frozenlist. + * @dev The zero address is the ERC-20 mint/burn sentinel, not a participant. Listing it would + * make the MANDATORY ERC-2980 getter `whitelist(address(0))` return `true`. Mint/burn + * permission is governed by the explicit `allowMint` / `allowBurn` flags instead. + */ + error RuleERC2980_ZeroAddressNotAllowed(); + /** + * @notice Thrown when adding an address that is already on the whitelist. + */ + error RuleERC2980_AddressAlreadyWhitelisted(); + /** + * @notice Thrown when removing an address that is not on the whitelist. + */ + error RuleERC2980_AddressNotWhitelisted(); + /** + * @notice Thrown when adding an address that is already on the frozenlist. + */ + error RuleERC2980_AddressAlreadyFrozen(); + /** + * @notice Thrown when removing an address that is not on the frozenlist. + */ + error RuleERC2980_AddressNotFrozen(); } diff --git a/src/rules/validation/abstract/base/RuleBlacklistBase.sol b/src/rules/validation/abstract/base/RuleBlacklistBase.sol index e066d95..4bb111b 100644 --- a/src/rules/validation/abstract/base/RuleBlacklistBase.sol +++ b/src/rules/validation/abstract/base/RuleBlacklistBase.sol @@ -5,6 +5,7 @@ import {RuleAddressSet} from "../RuleAddressSet/RuleAddressSet.sol"; import {RuleNFTAdapter} from "../core/RuleNFTAdapter.sol"; import {RuleTransferValidation} from "../core/RuleTransferValidation.sol"; import {RuleBlacklistInvariantStorage} from "../RuleAddressSet/invariantStorage/RuleBlacklistInvariantStorage.sol"; +import {AddressListInterfaceId} from "../../../interfaces/library/AddressListInterfaceId.sol"; import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; import {IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; @@ -19,6 +20,10 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the blacklist rule base. + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address for meta-transactions. + */ constructor(address forwarderIrrevocable) RuleAddressSet(forwarderIrrevocable) {} /*////////////////////////////////////////////////////////////// @@ -51,6 +56,9 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack _transferredFrom(spender, from, to, value); } + /** + * @inheritdoc IRule + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) public pure @@ -62,6 +70,9 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack || restrictionCode == CODE_ADDRESS_SPENDER_IS_BLACKLISTED; } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) public pure @@ -80,14 +91,26 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack } } + /** + * @inheritdoc RuleTransferValidation + */ function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { - return RuleTransferValidation.supportsInterface(interfaceId); + // Advertise IAddressList: this rule manages an address set and is callable through + // the IAddressList interface. + return interfaceId == AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID + || RuleTransferValidation.supportsInterface(interfaceId); } /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Detects whether a direct transfer is restricted by the blacklist. + * @param from The sender address. + * @param to The recipient address. + * @return The restriction code, or TRANSFER_OK if neither party is blacklisted. + */ function _detectTransferRestriction( address from, address to, @@ -106,6 +129,14 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Detects whether a delegated transfer is restricted by the blacklist. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + * @return The restriction code, or TRANSFER_OK if no party is blacklisted. + */ function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) internal view @@ -118,6 +149,12 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack return _detectTransferRestriction(from, to, value); } + /** + * @notice Reverts if a direct transfer is blocked by the blacklist. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferred(address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -126,6 +163,13 @@ abstract contract RuleBlacklistBase is RuleAddressSet, RuleNFTAdapter, RuleBlack ); } + /** + * @notice Reverts if a delegated transfer is blocked by the blacklist. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( diff --git a/src/rules/validation/abstract/base/RuleERC2980Base.sol b/src/rules/validation/abstract/base/RuleERC2980Base.sol index 1061824..e8e1ebb 100644 --- a/src/rules/validation/abstract/base/RuleERC2980Base.sol +++ b/src/rules/validation/abstract/base/RuleERC2980Base.sol @@ -28,8 +28,8 @@ import {IRule} from "RuleEngine/interfaces/IRule.sol"; */ abstract contract RuleERC2980Base is MetaTxModuleStandalone, - RuleERC2980Internal, RuleERC2980InvariantStorage, + RuleERC2980Internal, RuleNFTAdapter, IERC2980, IIdentityRegistryVerified @@ -38,17 +38,45 @@ abstract contract RuleERC2980Base is CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(address forwarderIrrevocable, bool allowBurn) MetaTxModuleStandalone(forwarderIrrevocable) { - if (allowBurn) { - _addWhitelistAddress(address(0)); - emit AddWhitelistAddress(address(0)); - } + /** + * @notice Whether this rule permits minting (`from == address(0)`). + * @dev Mint/burn permission is an EXPLICIT flag, never whitelist membership of `address(0)`. + * The zero address is the ERC-20 sentinel, not a participant: whitelisting it made the + * MANDATORY ERC-2980 getter `whitelist(address(0))` return `true`, a spec violation. + * A permitted mint still requires the RECIPIENT to be whitelisted and not frozen. + */ + bool public allowMint; + + /** + * @notice Whether this rule permits burning (`to == address(0)`). + * @dev See {allowMint}. A permitted burn still requires the SENDER not to be frozen. + */ + bool public allowBurn; + + /** + * @notice Initializes the rule. + * @dev `allowMintBurn` sets BOTH {allowMint} and {allowBurn} — the common case, since mint and + * burn are normally permitted. Use {setAllowMint} / {setAllowBurn} afterwards for + * independent control (e.g. to permanently close issuance while still allowing redemptions). + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address, set permanently at deployment. + * @param allowMintBurn When true, permits both minting and burning. + */ + constructor(address forwarderIrrevocable, bool allowMintBurn) MetaTxModuleStandalone(forwarderIrrevocable) { + allowMint = allowMintBurn; + allowBurn = allowMintBurn; + emit AllowMintUpdated(allowMintBurn); + emit AllowBurnUpdated(allowMintBurn); } /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + modifier onlyMintBurnManager() { + _authorizeMintBurnManager(); + _; + } + modifier onlyWhitelistAdd() { _authorizeWhitelistAdd(); _; @@ -69,11 +97,6 @@ abstract contract RuleERC2980Base is _; } - function _authorizeWhitelistAdd() internal view virtual; - function _authorizeWhitelistRemove() internal view virtual; - function _authorizeFrozenlistAdd() internal view virtual; - function _authorizeFrozenlistRemove() internal view virtual; - /*////////////////////////////////////////////////////////////// WHITELIST MANAGEMENT //////////////////////////////////////////////////////////////*/ @@ -81,6 +104,7 @@ abstract contract RuleERC2980Base is /** * @notice Adds multiple addresses to the whitelist. * @dev Does not revert if an address is already listed. + * @param targetAddresses Addresses to add to the whitelist. */ function addWhitelistAddresses(address[] calldata targetAddresses) public onlyWhitelistAdd { _addWhitelistAddresses(targetAddresses); @@ -90,6 +114,7 @@ abstract contract RuleERC2980Base is /** * @notice Removes multiple addresses from the whitelist. * @dev Does not revert if an address is not listed. + * @param targetAddresses Addresses to remove from the whitelist. */ function removeWhitelistAddresses(address[] calldata targetAddresses) public onlyWhitelistRemove { _removeWhitelistAddresses(targetAddresses); @@ -103,9 +128,11 @@ abstract contract RuleERC2980Base is * Deviation from ERC-2980 `Whitelistable` example interface: the spec's `addAddressToWhitelist` * returns `false` on duplicates instead of reverting. This implementation follows the codebase * convention of reverting on invalid single-item operations. + * @param targetAddress Address to add to the whitelist. */ function addWhitelistAddress(address targetAddress) public onlyWhitelistAdd { - require(!_isWhitelisted(targetAddress), RuleERC2980_AddressAlreadyListed()); + require(targetAddress != address(0), RuleERC2980_ZeroAddressNotAllowed()); + require(!_isWhitelisted(targetAddress), RuleERC2980_AddressAlreadyWhitelisted()); _addWhitelistAddress(targetAddress); emit AddWhitelistAddress(targetAddress); } @@ -117,9 +144,10 @@ abstract contract RuleERC2980Base is * Deviation from ERC-2980 `Whitelistable` example interface: the spec's `removeAddressFromWhitelist` * returns `false` when not found instead of reverting. This implementation follows the codebase * convention of reverting on invalid single-item operations. + * @param targetAddress Address to remove from the whitelist. */ function removeWhitelistAddress(address targetAddress) public onlyWhitelistRemove { - require(_isWhitelisted(targetAddress), RuleERC2980_AddressNotFound()); + require(_isWhitelisted(targetAddress), RuleERC2980_AddressNotWhitelisted()); _removeWhitelistAddress(targetAddress); emit RemoveWhitelistAddress(targetAddress); } @@ -131,6 +159,7 @@ abstract contract RuleERC2980Base is /** * @notice Adds multiple addresses to the frozenlist. * @dev Does not revert if an address is already listed. + * @param targetAddresses Addresses to add to the frozenlist. */ function addFrozenlistAddresses(address[] calldata targetAddresses) public onlyFrozenlistAdd { _addFrozenlistAddresses(targetAddresses); @@ -140,6 +169,7 @@ abstract contract RuleERC2980Base is /** * @notice Removes multiple addresses from the frozenlist. * @dev Does not revert if an address is not listed. + * @param targetAddresses Addresses to remove from the frozenlist. */ function removeFrozenlistAddresses(address[] calldata targetAddresses) public onlyFrozenlistRemove { _removeFrozenlistAddresses(targetAddresses); @@ -153,9 +183,11 @@ abstract contract RuleERC2980Base is * Deviation from ERC-2980 `Freezable` example interface: the spec's `addAddressToFrozenlist` * returns `false` on duplicates instead of reverting. This implementation follows the codebase * convention of reverting on invalid single-item operations. + * @param targetAddress Address to add to the frozenlist. */ function addFrozenlistAddress(address targetAddress) public onlyFrozenlistAdd { - require(!_isFrozen(targetAddress), RuleERC2980_AddressAlreadyListed()); + require(targetAddress != address(0), RuleERC2980_ZeroAddressNotAllowed()); + require(!_isFrozen(targetAddress), RuleERC2980_AddressAlreadyFrozen()); _addFrozenlistAddress(targetAddress); emit AddFrozenlistAddress(targetAddress); } @@ -167,9 +199,10 @@ abstract contract RuleERC2980Base is * Deviation from ERC-2980 `Freezable` example interface: the spec's `removeAddressFromFrozenlist` * returns `false` when not found instead of reverting. This implementation follows the codebase * convention of reverting on invalid single-item operations. + * @param targetAddress Address to remove from the frozenlist. */ function removeFrozenlistAddress(address targetAddress) public onlyFrozenlistRemove { - require(_isFrozen(targetAddress), RuleERC2980_AddressNotFound()); + require(_isFrozen(targetAddress), RuleERC2980_AddressNotFrozen()); _removeFrozenlistAddress(targetAddress); emit RemoveFrozenlistAddress(targetAddress); } @@ -178,6 +211,27 @@ abstract contract RuleERC2980Base is PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Enables or disables minting through this rule. + * @param value The new value of the `allowMint` flag. + */ + function setAllowMint(bool value) public virtual onlyMintBurnManager { + allowMint = value; + emit AllowMintUpdated(value); + } + + /** + * @notice Enables or disables burning through this rule. + * @param value The new value of the `allowBurn` flag. + */ + function setAllowBurn(bool value) public virtual onlyMintBurnManager { + allowBurn = value; + emit AllowBurnUpdated(value); + } + + /** + * @inheritdoc IERC3643IComplianceContract + */ function transferred(address from, address to, uint256 value) public view @@ -187,6 +241,9 @@ abstract contract RuleERC2980Base is _transferred(from, to, value); } + /** + * @inheritdoc IRuleEngine + */ function transferred(address spender, address from, address to, uint256 value) public view @@ -196,6 +253,9 @@ abstract contract RuleERC2980Base is _transferredFrom(spender, from, to, value); } + /** + * @inheritdoc IRule + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) public pure @@ -204,9 +264,13 @@ abstract contract RuleERC2980Base is returns (bool) { return restrictionCode == CODE_ADDRESS_FROM_IS_FROZEN || restrictionCode == CODE_ADDRESS_TO_IS_FROZEN - || restrictionCode == CODE_ADDRESS_SPENDER_IS_FROZEN || restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED; + || restrictionCode == CODE_ADDRESS_SPENDER_IS_FROZEN || restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED + || restrictionCode == CODE_MINT_NOT_ALLOWED || restrictionCode == CODE_BURN_NOT_ALLOWED; } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) public pure @@ -222,17 +286,25 @@ abstract contract RuleERC2980Base is return TEXT_ADDRESS_SPENDER_IS_FROZEN; } else if (restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED) { return TEXT_ADDRESS_TO_NOT_WHITELISTED; + } else if (restrictionCode == CODE_MINT_NOT_ALLOWED) { + return TEXT_MINT_NOT_ALLOWED; + } else if (restrictionCode == CODE_BURN_NOT_ALLOWED) { + return TEXT_BURN_NOT_ALLOWED; } else { return TEXT_CODE_NOT_FOUND; } } + /** + * @inheritdoc RuleTransferValidation + */ function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { return RuleTransferValidation.supportsInterface(interfaceId); } /** * @notice Returns the number of whitelisted addresses. + * @return The count of addresses currently in the whitelist. */ function whitelistAddressCount() public view returns (uint256) { return _whitelistCount(); @@ -240,6 +312,8 @@ abstract contract RuleERC2980Base is /** * @notice Returns true if the address is in the whitelist. + * @param targetAddress Address to check. + * @return True if the address is whitelisted. */ function isWhitelisted(address targetAddress) public view returns (bool) { return _isWhitelisted(targetAddress); @@ -247,6 +321,8 @@ abstract contract RuleERC2980Base is /** * @notice ERC-2980 getter: returns true if the address is whitelisted. + * @param _operator Address to check. + * @return True if the address is whitelisted. */ function whitelist(address _operator) public view virtual override(IERC2980) returns (bool) { return _isWhitelisted(_operator); @@ -256,6 +332,8 @@ abstract contract RuleERC2980Base is * @notice Returns true if the address is whitelisted (identity-verified). * @dev Reflects whitelist membership only. Frozen status is intentionally excluded: * freezing is a temporary enforcement action and does not revoke identity verification. + * @param targetAddress Address to check. + * @return True if the address is whitelisted. */ function isVerified(address targetAddress) public view virtual override(IIdentityRegistryVerified) returns (bool) { return _isWhitelisted(targetAddress); @@ -263,6 +341,8 @@ abstract contract RuleERC2980Base is /** * @notice Checks multiple addresses for whitelist membership. + * @param targetAddresses Addresses to check. + * @return results Array of booleans, true where the corresponding address is whitelisted. */ function areWhitelisted(address[] memory targetAddresses) public view returns (bool[] memory results) { results = new bool[](targetAddresses.length); @@ -273,6 +353,7 @@ abstract contract RuleERC2980Base is /** * @notice Returns the number of frozen addresses. + * @return The count of addresses currently in the frozenlist. */ function frozenlistAddressCount() public view returns (uint256) { return _frozenlistCount(); @@ -280,6 +361,8 @@ abstract contract RuleERC2980Base is /** * @notice Returns true if the address is in the frozenlist. + * @param targetAddress Address to check. + * @return True if the address is frozen. */ function isFrozen(address targetAddress) public view returns (bool) { return _isFrozen(targetAddress); @@ -287,6 +370,8 @@ abstract contract RuleERC2980Base is /** * @notice ERC-2980 getter: returns true if the address is frozen. + * @param _operator Address to check. + * @return True if the address is frozen. */ function frozenlist(address _operator) public view virtual override(IERC2980) returns (bool) { return _isFrozen(_operator); @@ -294,6 +379,8 @@ abstract contract RuleERC2980Base is /** * @notice Checks multiple addresses for frozenlist membership. + * @param targetAddresses Addresses to check. + * @return results Array of booleans, true where the corresponding address is frozen. */ function areFrozen(address[] memory targetAddresses) public view returns (bool[] memory results) { results = new bool[](targetAddresses.length); @@ -306,6 +393,31 @@ abstract contract RuleERC2980Base is INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Authorization hook invoked before toggling `allowMint` / `allowBurn`. + */ + function _authorizeMintBurnManager() internal view virtual; + + /** + * @notice Authorization hook invoked before adding addresses to the whitelist. + */ + function _authorizeWhitelistAdd() internal view virtual; + /** + * @notice Authorization hook invoked before removing addresses from the whitelist. + */ + function _authorizeWhitelistRemove() internal view virtual; + /** + * @notice Authorization hook invoked before adding addresses to the frozenlist. + */ + function _authorizeFrozenlistAdd() internal view virtual; + /** + * @notice Authorization hook invoked before removing addresses from the frozenlist. + */ + function _authorizeFrozenlistRemove() internal view virtual; + + /** + * @inheritdoc RuleTransferValidation + */ function _detectTransferRestriction( address from, address to, @@ -317,19 +429,34 @@ abstract contract RuleERC2980Base is override returns (uint8) { - // Frozenlist check has priority - if (_isFrozen(from)) { + bool isMint = from == address(0); + bool isBurn = to == address(0); + + // Gate the mint/burn OPERATION explicitly, rather than by whitelisting the zero address. + if (isMint && !allowMint) { + return CODE_MINT_NOT_ALLOWED; + } + if (isBurn && !allowBurn) { + return CODE_BURN_NOT_ALLOWED; + } + + // Frozenlist check has priority — but only for REAL participants. + if (!isMint && _isFrozen(from)) { return CODE_ADDRESS_FROM_IS_FROZEN; - } else if (_isFrozen(to)) { + } + if (!isBurn && _isFrozen(to)) { return CODE_ADDRESS_TO_IS_FROZEN; } - // Whitelist check: only the recipient must be whitelisted - if (!_isWhitelisted(to)) { + // Whitelist check: only the recipient must be whitelisted (ERC-2980); no recipient on a burn. + if (!isBurn && !_isWhitelisted(to)) { return CODE_ADDRESS_TO_NOT_WHITELISTED; } return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @inheritdoc RuleTransferValidation + */ function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) internal view @@ -343,6 +470,9 @@ abstract contract RuleERC2980Base is return _detectTransferRestriction(from, to, value); } + /** + * @inheritdoc RuleNFTAdapter + */ function _transferred(address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -351,6 +481,9 @@ abstract contract RuleERC2980Base is ); } + /** + * @inheritdoc RuleNFTAdapter + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( @@ -359,14 +492,23 @@ abstract contract RuleERC2980Base is ); } + /** + * @inheritdoc ERC2771Context + */ function _msgSender() internal view virtual override(ERC2771Context) returns (address sender) { return ERC2771Context._msgSender(); } + /** + * @inheritdoc ERC2771Context + */ function _msgData() internal view virtual override(ERC2771Context) returns (bytes calldata) { return ERC2771Context._msgData(); } + /** + * @inheritdoc ERC2771Context + */ function _contextSuffixLength() internal view virtual override(ERC2771Context) returns (uint256) { return ERC2771Context._contextSuffixLength(); } diff --git a/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol b/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol index cbb2b95..b07ad45 100644 --- a/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol +++ b/src/rules/validation/abstract/base/RuleIdentityRegistryBase.sol @@ -11,19 +11,63 @@ import {IIdentityRegistryVerified} from "../../../interfaces/IIdentityRegistry.s /** * @title RuleIdentityRegistryBase * @notice Checks the ERC-3643 Identity Registry for transfer participants when configured. - * @dev Burns (to == address(0)) are allowed even if the sender is not verified. + * @dev **ERC-3643 conformant by default.** The specification mandates that ONLY THE RECEIVER be + * identity-verified: + * + * - "The receiver MUST be whitelisted on the Identity Registry and verified" (§ Transfer) + * - "`transferFrom` works the same way" (§ Transfer) + * - "`mint` and `forcedTransfer` only require the receiver to be whitelisted + * and verified on the Identity Registry" (§ Transfer) + * - "The `burn` function bypasses all checks on eligibility" (§ Transfer) + * + * The sender, the spender and the minter are NOT required to be verified. Checking the sender + * in particular would TRAP DE-LISTED HOLDERS: ERC-3643 screens only the receiver precisely so + * that an investor whose identity lapses (expired claim, revoked identity) can still exit their + * position by sending to a verified counterparty. + * + * Stricter screening remains available, but as an EXPLICIT OPT-IN ({checkSender}, + * {checkSpender}) rather than an undocumented default. */ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegistryInvariantStorage { + /** + * @notice The ERC-3643 Identity Registry consulted to verify transfer participants; the zero address disables checks. + */ IIdentityRegistryVerified public identityRegistry; + /** + * @notice When true, ALSO require the sender to be identity-verified. + * @dev Defaults to FALSE: ERC-3643 does not require it. Enabling it is stricter than the + * specification and prevents a de-listed holder from exiting their position. + */ + bool public checkSender; + + /** + * @notice When true, ALSO require the spender to be identity-verified on `transferFrom`. + * @dev Defaults to FALSE: ERC-3643 does not require it ("`transferFrom` works the same way"). + * Mint and burn are exempt from this check regardless — the minter/burner acts on its own + * authority, not as a delegated ERC-20 spender. + */ + bool public checkSpender; + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(address identityRegistry_) { + /** + * @notice Initializes the rule with an optional identity registry. + * @dev Pass `false, false` for the ERC-3643-conformant default (only the receiver is verified). + * @param identityRegistry_ Identity registry address; when the zero address, the registry is left unset (checks disabled). + * @param checkSender_ When true, also verify the sender (STRICTER than ERC-3643). + * @param checkSpender_ When true, also verify the spender on `transferFrom` (STRICTER than ERC-3643). + */ + constructor(address identityRegistry_, bool checkSender_, bool checkSpender_) { if (identityRegistry_ != address(0)) { identityRegistry = IIdentityRegistryVerified(identityRegistry_); } + checkSender = checkSender_; + checkSpender = checkSpender_; + emit IdentityCheckSenderUpdated(checkSender_); + emit IdentityCheckSpenderUpdated(checkSpender_); } /*////////////////////////////////////////////////////////////// @@ -35,12 +79,15 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist _; } - function _authorizeIdentityRegistryManager() internal view virtual; - /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns whether this rule can produce the given restriction code. + * @param restrictionCode Restriction code to test. + * @return True if `restrictionCode` is one of this rule's identity-verification codes. + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { return restrictionCode == CODE_ADDRESS_FROM_NOT_VERIFIED || restrictionCode == CODE_ADDRESS_TO_NOT_VERIFIED || restrictionCode == CODE_ADDRESS_SPENDER_NOT_VERIFIED; @@ -50,25 +97,62 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the identity registry consulted during transfer checks. + * @param newRegistry New identity registry address; must not be the zero address. + */ function setIdentityRegistry(address newRegistry) public onlyIdentityRegistryManager { require(newRegistry != address(0), RuleIdentityRegistry_RegistryAddressZeroNotAllowed()); identityRegistry = IIdentityRegistryVerified(newRegistry); emit IdentityRegistryUpdated(newRegistry); } + /** + * @notice Enables or disables the (non-ERC-3643) sender verification check. + * @dev STRICTER than ERC-3643, which verifies only the receiver. Enabling this prevents a + * de-listed holder from exiting their position. + * @param value The new value of the `checkSender` flag. + */ + function setCheckSender(bool value) public virtual onlyIdentityRegistryManager { + checkSender = value; + emit IdentityCheckSenderUpdated(value); + } + + /** + * @notice Enables or disables the (non-ERC-3643) spender verification check on `transferFrom`. + * @dev STRICTER than ERC-3643. Mint and burn remain exempt regardless. + * @param value The new value of the `checkSpender` flag. + */ + function setCheckSpender(bool value) public virtual onlyIdentityRegistryManager { + checkSpender = value; + emit IdentityCheckSpenderUpdated(value); + } + + /** + * @notice Clears the identity registry, disabling identity checks (all transfers pass this rule). + */ function clearIdentityRegistry() public onlyIdentityRegistryManager { identityRegistry = IIdentityRegistryVerified(address(0)); emit IdentityRegistryUpdated(address(0)); } + /** + * @inheritdoc IERC3643IComplianceContract + */ function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { _transferred(from, to, value); } + /** + * @inheritdoc IRuleEngine + */ function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { _transferredFrom(spender, from, to, value); } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) public pure @@ -89,6 +173,17 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Authorization hook invoked before updating or clearing the identity registry. + */ + function _authorizeIdentityRegistryManager() internal view virtual; + + /** + * @notice Detects the restriction code for a direct transfer, verifying `from` and `to` against the registry. + * @param from Sender address; must be verified unless it is the zero address (mint). + * @param to Recipient address; must be verified unless it is the zero address (burn). + * @return The applicable restriction code, or TRANSFER_OK when no restriction applies. + */ function _detectTransferRestriction( address from, address to, @@ -102,19 +197,32 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist if (address(identityRegistry) == address(0)) { return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + // ERC-3643: "The `burn` function bypasses all checks on eligibility." if (to == address(0)) { return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } - if (from != address(0) && !identityRegistry.isVerified(from)) { + // OPT-IN, stricter than ERC-3643. Mints carry no sender, so they are exempt. + if (checkSender && from != address(0) && !identityRegistry.isVerified(from)) { return CODE_ADDRESS_FROM_NOT_VERIFIED; } - if (to != address(0) && !identityRegistry.isVerified(to)) { + + // MANDATED by ERC-3643: the receiver must be verified. This is the only required check, + // and it applies identically to `transfer`, `transferFrom` and `mint`. + if (!identityRegistry.isVerified(to)) { return CODE_ADDRESS_TO_NOT_VERIFIED; } return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Detects the restriction code for a `transferFrom`, verifying `spender` and delegating to the direct check. + * @param spender Approved spender initiating the transfer; must be verified unless it is the zero address. + * @param from Sender address, forwarded to the direct transfer check. + * @param to Recipient address, forwarded to the direct transfer check. + * @param value Transfer amount, forwarded to the direct transfer check. + * @return The applicable restriction code, or TRANSFER_OK when no restriction applies. + */ function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) internal view @@ -124,15 +232,27 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist if (address(identityRegistry) == address(0)) { return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + // ERC-3643: burn bypasses all eligibility checks. if (to == address(0)) { return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } - if (spender != address(0) && !identityRegistry.isVerified(spender)) { + + // OPT-IN, stricter than ERC-3643 ("`transferFrom` works the same way" — receiver only). + // Mint (from == 0) and burn (to == 0) are exempt: the minter/burner acts on its own + // authority, not as a delegated ERC-20 spender. This is what makes an unverified MINTER + // able to mint to a verified recipient, exactly as the specification requires. + if ( + checkSpender && spender != address(0) && from != address(0) && to != address(0) + && !identityRegistry.isVerified(spender) + ) { return CODE_ADDRESS_SPENDER_NOT_VERIFIED; } return _detectTransferRestriction(from, to, value); } + /** + * @inheritdoc RuleNFTAdapter + */ function _transferred(address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -141,6 +261,9 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist ); } + /** + * @inheritdoc RuleNFTAdapter + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( @@ -148,5 +271,4 @@ abstract contract RuleIdentityRegistryBase is RuleNFTAdapter, RuleIdentityRegist RuleIdentityRegistry_InvalidTransferFrom(address(this), spender, from, to, value, code) ); } - } diff --git a/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol b/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol index 8aebb27..12297aa 100644 --- a/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol +++ b/src/rules/validation/abstract/base/RuleMaxTotalSupplyBase.sol @@ -13,14 +13,24 @@ import {RuleTransferValidation} from "../core/RuleTransferValidation.sol"; * @notice Restricts minting so that total supply never exceeds a maximum value. */ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotalSupplyInvariantStorage { - /// @dev tokenContract is trusted to return a correct totalSupply. + /** + * @dev tokenContract is trusted to return a correct totalSupply. + */ ITotalSupply public tokenContract; + /** + * @notice Maximum total supply; minting that would exceed this value is rejected. + */ uint256 public maxTotalSupply; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Initializes the rule with the token to observe and the supply cap. + * @param tokenContract_ Address of the token whose `totalSupply` is checked; must not be the zero address. + * @param maxTotalSupply_ Maximum total supply allowed. + */ constructor(address tokenContract_, uint256 maxTotalSupply_) { require(tokenContract_ != address(0), RuleMaxTotalSupply_TokenAddressZeroNotAllowed()); tokenContract = ITotalSupply(tokenContract_); @@ -31,6 +41,11 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns whether this rule can produce the given restriction code. + * @param restrictionCode Restriction code to test. + * @return True if `restrictionCode` is the max-total-supply-exceeded code. + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { return restrictionCode == CODE_MAX_TOTAL_SUPPLY_EXCEEDED; } @@ -39,25 +54,42 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Updates the maximum total supply. + * @param newMaxTotalSupply New maximum total supply value. + */ function setMaxTotalSupply(uint256 newMaxTotalSupply) public onlyMaxTotalSupplyManager { maxTotalSupply = newMaxTotalSupply; emit MaxTotalSupplyUpdated(newMaxTotalSupply); } + /** + * @notice Updates the token contract whose total supply is checked. + * @param newTokenContract New token contract address; must not be the zero address. + */ function setTokenContract(address newTokenContract) public onlyMaxTotalSupplyManager { require(newTokenContract != address(0), RuleMaxTotalSupply_TokenAddressZeroNotAllowed()); tokenContract = ITotalSupply(newTokenContract); emit TokenContractUpdated(newTokenContract); } + /** + * @inheritdoc IERC3643IComplianceContract + */ function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { _transferred(from, to, value); } + /** + * @inheritdoc IRuleEngine + */ function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { _transferredFrom(spender, from, to, value); } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) public pure @@ -79,12 +111,18 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal _; } + /** + * @notice Authorization hook invoked before updating the max total supply or token contract. + */ function _authorizeMaxTotalSupplyManager() internal view virtual; /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @inheritdoc RuleTransferValidation + */ function _detectTransferRestriction( address from, address, @@ -98,13 +136,18 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal { if (from == address(0)) { uint256 currentSupply = tokenContract.totalSupply(); - if (currentSupply + value > maxTotalSupply) { + // Overflow-safe: `currentSupply + value` could exceed uint256 and this is a + // MUST-NOT-revert ERC-1404/ERC-3643 view, so compare against the remaining headroom. + if (currentSupply > maxTotalSupply || value > maxTotalSupply - currentSupply) { return CODE_MAX_TOTAL_SUPPLY_EXCEEDED; } } return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @inheritdoc RuleTransferValidation + */ function _detectTransferRestrictionFrom(address, address from, address to, uint256 value) internal view @@ -114,6 +157,12 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal return _detectTransferRestriction(from, to, value); } + /** + * @notice Enforces the max-total-supply restriction for a direct transfer, reverting on violation. + * @param from Sender address; the zero address denotes a mint whose supply is checked. + * @param to Recipient address. + * @param value Transfer amount. + */ function _transferred(address from, address to, uint256 value) internal view virtual { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -122,6 +171,13 @@ abstract contract RuleMaxTotalSupplyBase is RuleTransferValidation, RuleMaxTotal ); } + /** + * @notice Enforces the max-total-supply restriction for a `transferFrom`, reverting on violation. + * @param spender Approved spender initiating the transfer. + * @param from Sender address; the zero address denotes a mint whose supply is checked. + * @param to Recipient address. + * @param value Transfer amount. + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( diff --git a/src/rules/validation/abstract/base/RuleSanctionsListBase.sol b/src/rules/validation/abstract/base/RuleSanctionsListBase.sol index 586b949..fa90c34 100644 --- a/src/rules/validation/abstract/base/RuleSanctionsListBase.sol +++ b/src/rules/validation/abstract/base/RuleSanctionsListBase.sol @@ -15,12 +15,20 @@ import {IRule} from "RuleEngine/interfaces/IRule.sol"; * @notice Compliance rule enforcing sanctions-screening for token transfers. */ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapter, RuleSanctionsListInvariantStorage { + /** + * @notice The sanctions oracle consulted on each transfer; unset disables screening. + */ ISanctionsList public sanctionsList; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the sanctions-list rule base and optionally sets the oracle. + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address for meta-transactions. + * @param sanctionContractOracle_ Initial sanctions oracle; skipped when the zero address. + */ constructor(address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) MetaTxModuleStandalone(forwarderIrrevocable) { @@ -33,6 +41,9 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @inheritdoc IRule + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override(IRule) returns (bool) { return restrictionCode == CODE_ADDRESS_FROM_IS_SANCTIONED || restrictionCode == CODE_ADDRESS_TO_IS_SANCTIONED || restrictionCode == CODE_ADDRESS_SPENDER_IS_SANCTIONED; @@ -42,23 +53,41 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets the sanctions oracle consulted on each transfer. + * @dev Restricted to the sanction-list manager; reverts on the zero address. + * @param sanctionContractOracle_ The new sanctions oracle address. + */ function setSanctionListOracle(ISanctionsList sanctionContractOracle_) public virtual onlySanctionListManager { require(address(sanctionContractOracle_) != address(0), RuleSanctionsList_OracleAddressZeroNotAllowed()); _setSanctionListOracle(sanctionContractOracle_); } + /** + * @notice Clears the sanctions oracle, disabling sanctions screening. + * @dev Restricted to the sanction-list manager. + */ function clearSanctionListOracle() public virtual onlySanctionListManager { _setSanctionListOracle(ISanctionsList(address(0))); } + /** + * @inheritdoc IERC3643IComplianceContract + */ function transferred(address from, address to, uint256 value) public view override(IERC3643IComplianceContract) { _transferred(from, to, value); } + /** + * @inheritdoc IRuleEngine + */ function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { _transferredFrom(spender, from, to, value); } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) public pure @@ -84,12 +113,31 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte _; } - function _authorizeSanctionListManager() internal view virtual; - /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Updates the stored sanctions oracle and emits {SetSanctionListOracle}. + * @param sanctionContractOracle_ The new sanctions oracle address (may be zero to disable). + */ + function _setSanctionListOracle(ISanctionsList sanctionContractOracle_) internal virtual { + sanctionsList = sanctionContractOracle_; + emit SetSanctionListOracle(sanctionContractOracle_); + } + + /** + * @notice Authorizes the caller as sanction-list manager; reverts otherwise. + * @dev Implemented by concrete subclasses with the desired access-control policy. + */ + function _authorizeSanctionListManager() internal view virtual; + + /** + * @notice Detects whether a direct transfer is restricted by the sanctions oracle. + * @param from The sender address. + * @param to The recipient address. + * @return The restriction code, or TRANSFER_OK when no party is sanctioned. + */ function _detectTransferRestriction( address from, address to, @@ -110,6 +158,14 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte return uint8(REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Detects whether a delegated transfer is restricted by the sanctions oracle. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + * @return The restriction code, or TRANSFER_OK when no party is sanctioned. + */ function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) internal view @@ -126,6 +182,12 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte return uint8(REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Reverts if a direct transfer is blocked by the sanctions oracle. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferred(address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -134,6 +196,13 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte ); } + /** + * @notice Reverts if a delegated transfer is blocked by the sanctions oracle. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( @@ -141,9 +210,4 @@ abstract contract RuleSanctionsListBase is MetaTxModuleStandalone, RuleNFTAdapte RuleSanctionsList_InvalidTransferFrom(address(this), spender, from, to, value, code) ); } - - function _setSanctionListOracle(ISanctionsList sanctionContractOracle_) internal virtual { - sanctionsList = sanctionContractOracle_; - emit SetSanctionListOracle(sanctionContractOracle_); - } } diff --git a/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol b/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol index cb37bdd..af9d05c 100644 --- a/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol +++ b/src/rules/validation/abstract/base/RuleSpenderWhitelistBase.sol @@ -3,7 +3,9 @@ pragma solidity ^0.8.20; import {RuleAddressSet} from "../RuleAddressSet/RuleAddressSet.sol"; import {RuleNFTAdapter} from "../core/RuleNFTAdapter.sol"; +import {RuleTransferValidation} from "../core/RuleTransferValidation.sol"; import {RuleSpenderWhitelistInvariantStorage} from "../invariant/RuleSpenderWhitelistInvariantStorage.sol"; +import {AddressListInterfaceId} from "../../../interfaces/library/AddressListInterfaceId.sol"; import {IERC1404, IERC1404Extend} from "CMTAT/interfaces/tokenization/draft-IERC1404.sol"; import {IERC3643IComplianceContract} from "CMTAT/interfaces/tokenization/IERC3643Partial.sol"; import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; @@ -18,12 +20,21 @@ abstract contract RuleSpenderWhitelistBase is RuleAddressSet, RuleNFTAdapter, Ru CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the spender-whitelist rule base. + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address for meta-transactions. + */ constructor(address forwarderIrrevocable) RuleAddressSet(forwarderIrrevocable) {} /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns whether this rule can emit the given restriction code. + * @param restrictionCode The restriction code to check. + * @return True if the code is produced by this rule. + */ function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool) { return restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED; } @@ -37,10 +48,16 @@ abstract contract RuleSpenderWhitelistBase is RuleAddressSet, RuleNFTAdapter, Ru */ function transferred(address, address, uint256) public view override(IERC3643IComplianceContract) {} + /** + * @inheritdoc IRuleEngine + */ function transferred(address spender, address from, address to, uint256 value) public view override(IRuleEngine) { _transferredFrom(spender, from, to, value); } + /** + * @inheritdoc IERC1404 + */ function messageForTransferRestriction(uint8 restrictionCode) public pure @@ -53,31 +70,64 @@ abstract contract RuleSpenderWhitelistBase is RuleAddressSet, RuleNFTAdapter, Ru return TEXT_CODE_NOT_FOUND; } + /** + * @inheritdoc RuleTransferValidation + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { + // Advertise IAddressList: this rule manages an address set and is callable through + // the IAddressList interface. + return interfaceId == AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID + || RuleTransferValidation.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Direct transfers are always accepted by this rule. + * @return Always TRANSFER_OK. + */ function _detectTransferRestriction(address, address, uint256) internal pure virtual override returns (uint8) { return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } - function _detectTransferRestrictionFrom(address spender, address, address, uint256) + /** + * @notice Detects whether a delegated transfer is blocked because the spender is not whitelisted. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @return The restriction code, or TRANSFER_OK when allowed. + */ + function _detectTransferRestrictionFrom(address spender, address from, address to, uint256) internal view virtual override returns (uint8) { - if (spender != address(0) && !_isAddressListed(spender)) { + // Mint (from == address(0)) and burn (to == address(0)) are exempt from the spender check: + // the minter/burner acts on its own authority, not as a delegated ERC-20 spender. + if (from != address(0) && to != address(0) && !_isAddressListed(spender)) { return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; } return uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice No-op: regular transfers are intentionally ignored by this rule. + */ function _transferred(address, address, uint256) internal view virtual override { // no-op: regular transfers are intentionally ignored by this rule } + /** + * @notice Reverts if a delegated transfer is blocked because the spender is not whitelisted. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( diff --git a/src/rules/validation/abstract/base/RuleWhitelistBase.sol b/src/rules/validation/abstract/base/RuleWhitelistBase.sol index 6cdb6cd..bb77666 100644 --- a/src/rules/validation/abstract/base/RuleWhitelistBase.sol +++ b/src/rules/validation/abstract/base/RuleWhitelistBase.sol @@ -5,6 +5,7 @@ import {RuleAddressSet} from "../RuleAddressSet/RuleAddressSet.sol"; import {RuleWhitelistShared} from "../core/RuleWhitelistShared.sol"; import {RuleTransferValidation} from "../core/RuleTransferValidation.sol"; import {IIdentityRegistryVerified} from "../../../interfaces/IIdentityRegistry.sol"; +import {AddressListInterfaceId} from "../../../interfaces/library/AddressListInterfaceId.sol"; /** * @title RuleWhitelistBase @@ -15,25 +16,43 @@ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIde CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the whitelist rule base. + * @dev `allowMintBurn` sets BOTH {allowMint} and {allowBurn} — the common case, since mint and + * burn are normally permitted for a whitelist rule. Use {setAllowMint} / {setAllowBurn} + * afterwards for independent control (e.g. to permanently close issuance while still + * allowing redemptions). + * @dev Mint/burn permission is an explicit flag and NO LONGER whitelists `address(0)`: the zero + * address is the ERC-20 sentinel, not a participant, and listing it made + * `isVerified(address(0))` return `true` in violation of ERC-3643. + * @param forwarderIrrevocable Trusted ERC-2771 forwarder address for meta-transactions. + * @param checkSpender_ Whether to also verify the spender on delegated transfers. + * @param allowMintBurn When true, permits both minting and burning. + */ constructor(address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) RuleAddressSet(forwarderIrrevocable) { checkSpender = checkSpender_; - if (allowMintBurn) { - _addAddress(address(0)); - emit AddAddress(address(0)); - } + _setAllowMintBurn(allowMintBurn, allowMintBurn); } /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Enables or disables spender verification on delegated transfers. + * @dev Restricted to the check-spender manager; emits {CheckSpenderUpdated}. + * @param value The new state of the `checkSpender` flag. + */ function setCheckSpender(bool value) public virtual onlyCheckSpenderManager { _setCheckSpender(value); emit CheckSpenderUpdated(value); } + /** + * @inheritdoc IIdentityRegistryVerified + */ function isVerified(address targetAddress) public view @@ -44,8 +63,14 @@ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIde isListed = _isAddressListed(targetAddress); } + /** + * @inheritdoc RuleTransferValidation + */ function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { - return RuleTransferValidation.supportsInterface(interfaceId); + // Advertise IAddressList: this rule manages an address set and is usable as a + // child rule of RuleWhitelistWrapper, which calls it through IAddressList. + return interfaceId == AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID + || RuleTransferValidation.supportsInterface(interfaceId); } /*////////////////////////////////////////////////////////////// @@ -57,12 +82,30 @@ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIde _; } - function _authorizeCheckSpenderManager() internal view virtual; - /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Internal helper to update the `checkSpender` flag. + * @param value New flag value. + */ + function _setCheckSpender(bool value) internal virtual { + checkSpender = value; + } + + /** + * @notice Authorizes the caller as check-spender manager; reverts otherwise. + * @dev Implemented by concrete subclasses with the desired access-control policy. + */ + function _authorizeCheckSpenderManager() internal view virtual; + + /** + * @notice Detects whether a direct transfer is restricted by the whitelist. + * @param from The sender address. + * @param to The recipient address. + * @return The restriction code, or TRANSFER_OK when both parties are whitelisted. + */ function _detectTransferRestriction( address from, address to, @@ -74,14 +117,34 @@ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIde override returns (uint8) { - if (!isAddressListed(from)) { + bool isMint = from == address(0); + bool isBurn = to == address(0); + + // Gate the mint/burn OPERATION explicitly, rather than by listing the zero address. + uint8 mintBurnCode = _detectMintBurnRestriction(from, to); + if (mintBurnCode != uint8(REJECTED_CODE_BASE.TRANSFER_OK)) { + return mintBurnCode; + } + + // Screen only the REAL participants. A permitted mint still requires a whitelisted + // recipient; a permitted burn still requires a whitelisted sender. + if (!isMint && !isAddressListed(from)) { return CODE_ADDRESS_FROM_NOT_WHITELISTED; - } else if (!isAddressListed(to)) { + } + if (!isBurn && !isAddressListed(to)) { return CODE_ADDRESS_TO_NOT_WHITELISTED; } return uint8(REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Detects whether a delegated transfer is restricted by the whitelist. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + * @return The restriction code, or TRANSFER_OK when allowed. + */ function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) internal view @@ -89,13 +152,11 @@ abstract contract RuleWhitelistBase is RuleAddressSet, RuleWhitelistShared, IIde override returns (uint8) { - if (checkSpender && !isAddressListed(spender)) { + // Mint (from == address(0)) and burn (to == address(0)) are exempt from the spender check: + // the minter/burner acts on its own authority, not as a delegated ERC-20 spender. + if (checkSpender && from != address(0) && to != address(0) && !isAddressListed(spender)) { return CODE_ADDRESS_SPENDER_NOT_WHITELISTED; } return _detectTransferRestriction(from, to, value); } - - function _setCheckSpender(bool value) internal virtual { - checkSpender = value; - } } diff --git a/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol b/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol index 0e3d188..6c95e3d 100644 --- a/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol +++ b/src/rules/validation/abstract/base/RuleWhitelistWrapperBase.sol @@ -26,10 +26,19 @@ abstract contract RuleWhitelistWrapperBase is CONSTRUCTOR //////////////////////////////////////////////////////////////*/ /** + * @notice Deploys the whitelist wrapper base. + * @dev The wrapper holds no addresses of its own — it ORs its child rules. It therefore needs its + * OWN mint/burn flags: the children no longer list `address(0)`, so without these a mint + * would resolve `from` as unlisted and be rejected. * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + * @param checkSpender_ Whether to also verify the spender on delegated transfers. + * @param allowMintBurn When true, permits both minting and burning. */ - constructor(address forwarderIrrevocable, bool checkSpender_) MetaTxModuleStandalone(forwarderIrrevocable) { + constructor(address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) + MetaTxModuleStandalone(forwarderIrrevocable) + { checkSpender = checkSpender_; + _setAllowMintBurn(allowMintBurn, allowMintBurn); } /*////////////////////////////////////////////////////////////// @@ -41,8 +50,6 @@ abstract contract RuleWhitelistWrapperBase is _; } - function _authorizeCheckSpenderManager() internal virtual; - /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -60,19 +67,18 @@ abstract contract RuleWhitelistWrapperBase is emit CheckSpenderUpdated(value); } - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(RuleTransferValidation) - returns (bool) - { + /** + * @inheritdoc RuleTransferValidation + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(RuleTransferValidation) returns (bool) { return RuleTransferValidation.supportsInterface(interfaceId); } /** * @notice Returns true if the address is listed in at least one child whitelist rule. * @dev Delegates to the same child-rule scan used by transfer restriction checks. + * @param targetAddress The address to check across all child whitelist rules. + * @return True if the address is listed in at least one child rule. */ function isVerified(address targetAddress) public view virtual override(IIdentityRegistryVerified) returns (bool) { address[] memory targets = new address[](1); @@ -85,6 +91,22 @@ abstract contract RuleWhitelistWrapperBase is INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Internal helper to update the `checkSpender` flag. + * @param value New flag value. + */ + function _setCheckSpender(bool value) internal virtual { + checkSpender = value; + } + + /** + * @notice Authorizes the caller as check-spender manager; reverts otherwise. + * @dev Implemented by concrete subclasses with the desired access-control policy. + * `view` by convention: an access-control hook checks and reverts, it never mutates state. + * Declaring it `view` makes that a compiler-enforced invariant rather than a convention. + */ + function _authorizeCheckSpenderManager() internal view virtual; + /** * @notice Go through all the whitelist rules to know if a restriction exists on the transfer * @param from the origin address @@ -103,6 +125,36 @@ abstract contract RuleWhitelistWrapperBase is override returns (uint8) { + // Gate the mint/burn OPERATION explicitly, before consulting any child rule. + uint8 mintBurnCode = _detectMintBurnRestriction(from, to); + if (mintBurnCode != uint8(REJECTED_CODE_BASE.TRANSFER_OK)) { + return mintBurnCode; + } + + bool isMint = from == address(0); + bool isBurn = to == address(0); + + // Resolve only the REAL participants against the children: the zero address is a sentinel, + // not a listed member of any child, so asking about it would always fail. + // Degenerate (0, 0): neither leg is a real participant, so there is nothing to screen. + // Handled explicitly so the wrapper and {RuleWhitelistBase} return the same answer — the two + // share `_detectMintBurnRestriction` precisely so they cannot drift. + if (isMint && isBurn) { + return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + } + if (isMint) { + if (!_isListedInAnyChild(to)) { + return CODE_ADDRESS_TO_NOT_WHITELISTED; + } + return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + } + if (isBurn) { + if (!_isListedInAnyChild(from)) { + return CODE_ADDRESS_FROM_NOT_WHITELISTED; + } + return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + } + address[] memory targetAddress = new address[](2); targetAddress[0] = from; targetAddress[1] = to; @@ -117,6 +169,25 @@ abstract contract RuleWhitelistWrapperBase is } } + /** + * @notice Returns true when `targetAddress` is listed in at least one child rule. + * @param targetAddress The address to resolve across the children. + * @return True if listed in any child. + */ + function _isListedInAnyChild(address targetAddress) internal view virtual returns (bool) { + address[] memory targets = new address[](1); + targets[0] = targetAddress; + return _detectTransferRestrictionForTargets(targets)[0]; + } + + /** + * @notice Go through all the whitelist rules to know if a delegated transfer is restricted. + * @param spender The delegated spender address. + * @param from The origin address. + * @param to The destination address. + * @param value The amount transferred. + * @return The restriction code or REJECTED_CODE_BASE.TRANSFER_OK. + */ function _detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) internal view @@ -124,7 +195,9 @@ abstract contract RuleWhitelistWrapperBase is override returns (uint8) { - if (!checkSpender) { + // Mint (from == address(0)) and burn (to == address(0)) are exempt from the spender check: + // the minter/burner acts on its own authority, not as a delegated ERC-20 spender. + if (!checkSpender || from == address(0) || to == address(0)) { return _detectTransferRestriction(from, to, value); } @@ -148,6 +221,12 @@ abstract contract RuleWhitelistWrapperBase is // ERC-7943 tokenId overloads are provided by {RuleNFTAdapter} via RuleWhitelistShared. + /** + * @notice Reverts if a direct transfer is blocked by any child whitelist rule. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferred(address from, address to, uint256 value) internal view @@ -157,6 +236,13 @@ abstract contract RuleWhitelistWrapperBase is RuleWhitelistShared._transferred(from, to, value); } + /** + * @notice Reverts if a delegated transfer is blocked by any child whitelist rule. + * @param spender The delegated spender address. + * @param from The sender address. + * @param to The recipient address. + * @param value The amount transferred. + */ function _transferred(address spender, address from, address to, uint256 value) internal view @@ -204,20 +290,13 @@ abstract contract RuleWhitelistWrapperBase is return result; } - /** - * @notice Internal helper to update the `checkSpender` flag. - * @param value New flag value. - */ - function _setCheckSpender(bool value) internal virtual { - checkSpender = value; - } - /*////////////////////////////////////////////////////////////// ERC-2771 //////////////////////////////////////////////////////////////*/ /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return sender The effective message sender, unwrapped from the meta-transaction if present. */ function _msgSender() internal view virtual override(ERC2771Context) returns (address sender) { return ERC2771Context._msgSender(); @@ -225,6 +304,7 @@ abstract contract RuleWhitelistWrapperBase is /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return The effective calldata, unwrapped from the meta-transaction if present. */ function _msgData() internal view virtual override(ERC2771Context) returns (bytes calldata) { return ERC2771Context._msgData(); @@ -232,6 +312,7 @@ abstract contract RuleWhitelistWrapperBase is /** * @dev This surcharge is not necessary if you do not use the MetaTxModule + * @return The length of the ERC-2771 context suffix appended to calldata. */ function _contextSuffixLength() internal view virtual override(ERC2771Context) returns (uint256) { return ERC2771Context._contextSuffixLength(); diff --git a/src/rules/validation/abstract/core/RuleNFTAdapter.sol b/src/rules/validation/abstract/core/RuleNFTAdapter.sol index db03caa..334dc01 100644 --- a/src/rules/validation/abstract/core/RuleNFTAdapter.sol +++ b/src/rules/validation/abstract/core/RuleNFTAdapter.sol @@ -17,13 +17,51 @@ import {ITransferContext} from "../../../interfaces/ITransferContext.sol"; * @dev Delegates tokenId overloads to RuleTransferValidation's internal hooks. */ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleComplianceExtend, ITransferContext { + /** + * @notice Selector of the ERC-3643 compliance `transferred` hook. + */ bytes4 internal constant TRANSFERRED_SELECTOR_ERC3643 = IERC3643IComplianceContract.transferred.selector; + /** + * @notice Selector of the RuleEngine `transferred` hook. + */ bytes4 internal constant TRANSFERRED_SELECTOR_RULE_ENGINE = IRuleEngine.transferred.selector; + /** + * @notice Selector of the ERC-7943 `transferred(from,to,tokenId,value)` hook. + */ bytes4 internal constant TRANSFERRED_SELECTOR_ERC7943 = bytes4(keccak256("transferred(address,address,uint256,uint256)")); + /** + * @notice Selector of the ERC-7943 `transferred(spender,from,to,tokenId,value)` hook. + */ bytes4 internal constant TRANSFERRED_SELECTOR_ERC7943_FROM = bytes4(keccak256("transferred(address,address,address,uint256,uint256)")); + /*////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc ITransferContext + */ + function transferred(MultiTokenTransferContext calldata ctx) external virtual override { + if (ctx.sender != address(0) && ctx.sender != ctx.from) { + _transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value); + } else { + _transferred(ctx.from, ctx.to, ctx.value); + } + } + + /** + * @inheritdoc ITransferContext + */ + function transferred(FungibleTransferContext calldata ctx) external virtual override { + if (ctx.sender != address(0) && ctx.sender != ctx.from) { + _transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value); + } else { + _transferred(ctx.from, ctx.to, ctx.value); + } + } + /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -31,7 +69,7 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC /** * @inheritdoc IERC7943NonFungibleComplianceExtend */ - function detectTransferRestriction( + function transferred( address from, address to, uint256, @@ -39,50 +77,53 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC uint256 value ) public - view virtual override(IERC7943NonFungibleComplianceExtend) - returns (uint8) { - return _detectTransferRestriction(from, to, value); + _transferred(from, to, value); } /** * @inheritdoc IERC7943NonFungibleComplianceExtend */ - function detectTransferRestrictionFrom( + function transferred( address spender, address from, address to, uint256, /* tokenId */ uint256 value - ) public view virtual override(IERC7943NonFungibleComplianceExtend) returns (uint8) { - return _detectTransferRestrictionFrom(spender, from, to, value); + ) + public + virtual + override(IERC7943NonFungibleComplianceExtend) + { + _transferredFrom(spender, from, to, value); } /** - * @inheritdoc IERC7943NonFungibleCompliance + * @inheritdoc IERC7943NonFungibleComplianceExtend */ - function canTransfer( + function detectTransferRestriction( address from, address to, uint256, /* tokenId */ - uint256 amount + uint256 value ) public view - override(IERC7943NonFungibleCompliance) - returns (bool) + virtual + override(IERC7943NonFungibleComplianceExtend) + returns (uint8) { - return _detectTransferRestriction(from, to, amount) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + return _detectTransferRestriction(from, to, value); } /** * @inheritdoc IERC7943NonFungibleComplianceExtend */ - function canTransferFrom( + function detectTransferRestrictionFrom( address spender, address from, address to, @@ -94,33 +135,33 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC view virtual override(IERC7943NonFungibleComplianceExtend) - returns (bool) + returns (uint8) { - return _detectTransferRestrictionFrom(spender, from, to, value) - == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); + return _detectTransferRestrictionFrom(spender, from, to, value); } /** - * @inheritdoc IERC7943NonFungibleComplianceExtend + * @inheritdoc IERC7943NonFungibleCompliance */ - function transferred( + function canTransfer( address from, address to, uint256, /* tokenId */ - uint256 value + uint256 amount ) public - virtual - override(IERC7943NonFungibleComplianceExtend) + view + override(IERC7943NonFungibleCompliance) + returns (bool) { - _transferred(from, to, value); + return _detectTransferRestriction(from, to, amount) == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } /** * @inheritdoc IERC7943NonFungibleComplianceExtend */ - function transferred( + function canTransferFrom( address spender, address from, address to, @@ -129,32 +170,13 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC uint256 value ) public + view virtual override(IERC7943NonFungibleComplianceExtend) + returns (bool) { - _transferredFrom(spender, from, to, value); - } - - /** - * @inheritdoc ITransferContext - */ - function transferred(MultiTokenTransferContext calldata ctx) external virtual override { - if (ctx.sender != address(0) && ctx.sender != ctx.from) { - _transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value); - } else { - _transferred(ctx.from, ctx.to, ctx.value); - } - } - - /** - * @inheritdoc ITransferContext - */ - function transferred(FungibleTransferContext calldata ctx) external virtual override { - if (ctx.sender != address(0) && ctx.sender != ctx.from) { - _transferredFrom(ctx.sender, ctx.from, ctx.to, ctx.value); - } else { - _transferred(ctx.from, ctx.to, ctx.value); - } + return _detectTransferRestrictionFrom(spender, from, to, value) + == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } /*////////////////////////////////////////////////////////////// @@ -163,11 +185,18 @@ abstract contract RuleNFTAdapter is RuleTransferValidation, IERC7943NonFungibleC /** * @notice Internal hook for post-transfer validation or state updates. + * @param from Address tokens are transferred from. + * @param to Address tokens are transferred to. + * @param value Amount transferred. */ function _transferred(address from, address to, uint256 value) internal virtual; /** * @notice Internal hook for post-transfer validation or state updates (spender-aware). + * @param spender Address executing the transfer on behalf of `from`. + * @param from Address tokens are transferred from. + * @param to Address tokens are transferred to. + * @param value Amount transferred. */ function _transferredFrom(address spender, address from, address to, uint256 value) internal virtual; } diff --git a/src/rules/validation/abstract/core/RuleTransferValidation.sol b/src/rules/validation/abstract/core/RuleTransferValidation.sol index ed0fa18..cacc280 100644 --- a/src/rules/validation/abstract/core/RuleTransferValidation.sol +++ b/src/rules/validation/abstract/core/RuleTransferValidation.sol @@ -15,6 +15,10 @@ import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; /* ==== Modules === */ import {VersionModule} from "../../../../modules/VersionModule.sol"; +/** + * @title RuleTransferValidation — base transfer-restriction checks and interface support for rules. + * @notice Exposes ERC-1404 / ERC-3643 / ERC-7551 read views delegating to internal restriction hooks. + */ abstract contract RuleTransferValidation is VersionModule, IERC1404Extend, @@ -83,11 +87,15 @@ abstract contract RuleTransferValidation is == uint8(IERC1404Extend.REJECTED_CODE_BASE.TRANSFER_OK); } + /** + * @notice Returns whether this contract implements the given interface. + * @param interfaceId The ERC-165 interface identifier to query. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { return interfaceId == RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID || interfaceId == ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID - || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID - || interfaceId == type(IERC7551Compliance).interfaceId + || interfaceId == RuleInterfaceId.IRULE_INTERFACE_ID || interfaceId == type(IERC7551Compliance).interfaceId || interfaceId == type(IERC3643IComplianceContract).interfaceId; } diff --git a/src/rules/validation/abstract/core/RuleWhitelistShared.sol b/src/rules/validation/abstract/core/RuleWhitelistShared.sol index b53fbad..7bb55b9 100644 --- a/src/rules/validation/abstract/core/RuleWhitelistShared.sol +++ b/src/rules/validation/abstract/core/RuleWhitelistShared.sol @@ -22,6 +22,32 @@ abstract contract RuleWhitelistShared is RuleNFTAdapter, RuleWhitelistInvariantS */ bool public checkSpender; + /** + * @notice Whether this rule permits minting (`from == address(0)`). + * @dev Mint/burn permission is an EXPLICIT flag, never list membership of `address(0)`. + * The zero address is the ERC-20 mint/burn sentinel, not a participant: listing it would + * make `isVerified(address(0))` return `true`, contradicting ERC-3643 (which defines + * `isVerified` as "is this wallet a valid investor holding the required claims"). + * Note this flag only gates the *operation*: a permitted mint still requires the RECIPIENT + * to be whitelisted, so `allowMint = true` is not a bypass. + */ + bool public allowMint; + + /** + * @notice Whether this rule permits burning (`to == address(0)`). + * @dev See {allowMint}. A permitted burn still requires the SENDER to be whitelisted. + */ + bool public allowBurn; + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + modifier onlyMintBurnManager() { + _authorizeMintBurnManager(); + _; + } + /*////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -36,7 +62,8 @@ abstract contract RuleWhitelistShared is RuleNFTAdapter, RuleWhitelistInvariantS function canReturnTransferRestrictionCode(uint8 restrictionCode) external pure override returns (bool isKnown) { return restrictionCode == CODE_ADDRESS_FROM_NOT_WHITELISTED || restrictionCode == CODE_ADDRESS_TO_NOT_WHITELISTED - || restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED; + || restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED || restrictionCode == CODE_MINT_NOT_ALLOWED + || restrictionCode == CODE_BURN_NOT_ALLOWED; } /** @@ -58,6 +85,10 @@ abstract contract RuleWhitelistShared is RuleNFTAdapter, RuleWhitelistInvariantS return TEXT_ADDRESS_TO_NOT_WHITELISTED; } else if (restrictionCode == CODE_ADDRESS_SPENDER_NOT_WHITELISTED) { return TEXT_ADDRESS_SPENDER_NOT_WHITELISTED; + } else if (restrictionCode == CODE_MINT_NOT_ALLOWED) { + return TEXT_MINT_NOT_ALLOWED; + } else if (restrictionCode == CODE_BURN_NOT_ALLOWED) { + return TEXT_BURN_NOT_ALLOWED; } else { return TEXT_CODE_NOT_FOUND; } @@ -67,6 +98,24 @@ abstract contract RuleWhitelistShared is RuleNFTAdapter, RuleWhitelistInvariantS PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Enables or disables minting through this rule. + * @param value The new value of the `allowMint` flag. + */ + function setAllowMint(bool value) public virtual onlyMintBurnManager { + allowMint = value; + emit AllowMintUpdated(value); + } + + /** + * @notice Enables or disables burning through this rule. + * @param value The new value of the `allowBurn` flag. + */ + function setAllowBurn(bool value) public virtual onlyMintBurnManager { + allowBurn = value; + emit AllowBurnUpdated(value); + } + /** * @notice ERC-3643 hook called when a transfer occurs. * @dev @@ -101,6 +150,43 @@ abstract contract RuleWhitelistShared is RuleNFTAdapter, RuleWhitelistInvariantS INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Sets both mint/burn flags at once (deployment helper). + * @param allowMint_ Whether minting is permitted. + * @param allowBurn_ Whether burning is permitted. + */ + function _setAllowMintBurn(bool allowMint_, bool allowBurn_) internal virtual { + allowMint = allowMint_; + allowBurn = allowBurn_; + emit AllowMintUpdated(allowMint_); + emit AllowBurnUpdated(allowBurn_); + } + + /** + * @notice Gates the mint/burn OPERATION, before any address is screened. + * @dev Shared by {RuleWhitelistBase} and {RuleWhitelistWrapperBase} so the two can never drift. + * @param from The sender (zero address for a mint). + * @param to The recipient (zero address for a burn). + * @return The restriction code, or TRANSFER_OK when the operation is permitted. + */ + function _detectMintBurnRestriction(address from, address to) internal view virtual returns (uint8) { + if (from == address(0) && !allowMint) { + return CODE_MINT_NOT_ALLOWED; + } + if (to == address(0) && !allowBurn) { + return CODE_BURN_NOT_ALLOWED; + } + return uint8(REJECTED_CODE_BASE.TRANSFER_OK); + } + + /** + * @notice Authorizes the caller to toggle `allowMint` / `allowBurn`; reverts otherwise. + */ + function _authorizeMintBurnManager() internal view virtual; + + /** + * @inheritdoc RuleNFTAdapter + */ function _transferred(address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestriction(from, to, value); require( @@ -109,6 +195,9 @@ abstract contract RuleWhitelistShared is RuleNFTAdapter, RuleWhitelistInvariantS ); } + /** + * @inheritdoc RuleNFTAdapter + */ function _transferredFrom(address spender, address from, address to, uint256 value) internal view virtual override { uint8 code = _detectTransferRestrictionFrom(spender, from, to, value); require( diff --git a/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol index e6adfd1..a90c621 100644 --- a/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol +++ b/src/rules/validation/abstract/invariant/RuleIdentityRegistryInvariantStorage.sol @@ -3,17 +3,54 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "./RuleSharedInvariantStorage.sol"; +/** + * @title RuleIdentityRegistryInvariantStorage — constants and event for the identity-registry rule. + */ abstract contract RuleIdentityRegistryInvariantStorage is RuleSharedInvariantStorage { + /** + * @notice Restriction message returned when the sender is not verified. + */ string constant TEXT_ADDRESS_FROM_NOT_VERIFIED = "The sender is not verified"; + /** + * @notice Restriction message returned when the recipient is not verified. + */ string constant TEXT_ADDRESS_TO_NOT_VERIFIED = "The recipient is not verified"; + /** + * @notice Restriction message returned when the spender is not verified. + */ string constant TEXT_ADDRESS_SPENDER_NOT_VERIFIED = "The spender is not verified"; // It is very important that each rule uses an unique code + /** + * @notice Restriction code returned when the sender is not verified. + */ uint8 public constant CODE_ADDRESS_FROM_NOT_VERIFIED = 55; + /** + * @notice Restriction code returned when the recipient is not verified. + */ uint8 public constant CODE_ADDRESS_TO_NOT_VERIFIED = 56; + /** + * @notice Restriction code returned when the spender is not verified. + */ uint8 public constant CODE_ADDRESS_SPENDER_NOT_VERIFIED = 57; + /** + * @notice Emitted when the identity registry address is updated. + * @param newRegistry Address of the newly configured identity registry. + */ event IdentityRegistryUpdated(address indexed newRegistry); + /** + * @notice Emitted when the identity rule's `checkSender` flag is updated. + * @param newValue New value of the `checkSender` flag. + */ + event IdentityCheckSenderUpdated(bool newValue); + /** + * @notice Emitted when the identity rule's `checkSpender` flag is updated. + * @dev Named distinctly from the whitelist rule's `CheckSpenderUpdated` so a contract may inherit + * both invariant-storage contracts (e.g. the test HelperContract) without a name clash. + * @param newValue New value of the `checkSpender` flag. + */ + event IdentityCheckSpenderUpdated(bool newValue); error RuleIdentityRegistry_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); error RuleIdentityRegistry_InvalidTransferFrom( diff --git a/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol index 310a1c5..02a8be5 100644 --- a/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol +++ b/src/rules/validation/abstract/invariant/RuleMaxTotalSupplyInvariantStorage.sol @@ -3,13 +3,30 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "./RuleSharedInvariantStorage.sol"; +/** + * @title RuleMaxTotalSupplyInvariantStorage — constants and events for the max-total-supply rule. + */ abstract contract RuleMaxTotalSupplyInvariantStorage is RuleSharedInvariantStorage { + /** + * @notice Restriction message returned when the max total supply would be exceeded. + */ string constant TEXT_MAX_TOTAL_SUPPLY_EXCEEDED = "Max total supply exceeded"; // It is very important that each rule uses an unique code + /** + * @notice Restriction code returned when the max total supply would be exceeded. + */ uint8 public constant CODE_MAX_TOTAL_SUPPLY_EXCEEDED = 50; + /** + * @notice Emitted when the maximum total supply is updated. + * @param newMaxTotalSupply New maximum total supply cap. + */ event MaxTotalSupplyUpdated(uint256 newMaxTotalSupply); + /** + * @notice Emitted when the tracked token contract is updated. + * @param newTokenContract Address of the newly configured token contract. + */ event TokenContractUpdated(address indexed newTokenContract); error RuleMaxTotalSupply_InvalidTransfer(address rule, address from, address to, uint256 value, uint8 code); diff --git a/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol index 10d8f06..c717f01 100644 --- a/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol +++ b/src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol @@ -5,22 +5,50 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "./RuleSharedInvariantStorage.sol"; import {ISanctionsList} from "../../../interfaces/ISanctionsList.sol"; +/** + * @title RuleSanctionsListInvariantStorage — constants, role, event and errors for the sanctions-list rule. + */ abstract contract RuleSanctionsListInvariantStorage is RuleSharedInvariantStorage { /* ============ Role ============ */ + /** + * @notice Role allowed to configure the sanctions-list oracle. + */ bytes32 public constant SANCTIONLIST_ROLE = keccak256("SANCTIONLIST_ROLE"); /* ============ String message ============ */ + /** + * @notice Restriction message returned when the sender is sanctioned. + */ string constant TEXT_ADDRESS_FROM_IS_SANCTIONED = "The sender is sanctioned"; + /** + * @notice Restriction message returned when the recipient is sanctioned. + */ string constant TEXT_ADDRESS_TO_IS_SANCTIONED = "The recipient is sanctioned"; + /** + * @notice Restriction message returned when the spender is sanctioned. + */ string constant TEXT_ADDRESS_SPENDER_IS_SANCTIONED = "The spender is sanctioned"; /* ============ Code ============ */ // It is very important that each rule uses an unique code + /** + * @notice Restriction code returned when the sender is sanctioned. + */ uint8 public constant CODE_ADDRESS_FROM_IS_SANCTIONED = 30; + /** + * @notice Restriction code returned when the recipient is sanctioned. + */ uint8 public constant CODE_ADDRESS_TO_IS_SANCTIONED = 31; + /** + * @notice Restriction code returned when the spender is sanctioned. + */ uint8 public constant CODE_ADDRESS_SPENDER_IS_SANCTIONED = 32; /* ============ Event ============ */ + /** + * @notice Emitted when the sanctions-list oracle is set. + * @param newOracle Address of the newly configured sanctions-list oracle. + */ event SetSanctionListOracle(ISanctionsList newOracle); /* ============ Custom errors ============ */ diff --git a/src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol index e04dabf..5a99fdf 100644 --- a/src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol +++ b/src/rules/validation/abstract/invariant/RuleSharedInvariantStorage.sol @@ -1,7 +1,13 @@ // SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; +/** + * @title RuleSharedInvariantStorage — constants shared across all rules. + */ abstract contract RuleSharedInvariantStorage { /* ============ String message ============ */ + /** + * @notice Message returned when a restriction code has no associated message. + */ string constant TEXT_CODE_NOT_FOUND = "Unknown restriction code"; } diff --git a/src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol b/src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol index dc8bd63..5898c4a 100644 --- a/src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol +++ b/src/rules/validation/abstract/invariant/RuleSpenderWhitelistInvariantStorage.sol @@ -3,9 +3,18 @@ pragma solidity ^0.8.20; import {RuleSharedInvariantStorage} from "./RuleSharedInvariantStorage.sol"; +/** + * @title RuleSpenderWhitelistInvariantStorage — constants and error for the spender-whitelist rule. + */ abstract contract RuleSpenderWhitelistInvariantStorage is RuleSharedInvariantStorage { // It is very important that each rule uses an unique code + /** + * @notice Restriction code returned when the spender is not whitelisted. + */ uint8 public constant CODE_ADDRESS_SPENDER_NOT_WHITELISTED = 66; + /** + * @notice Restriction message returned when the spender is not whitelisted. + */ string constant TEXT_ADDRESS_SPENDER_NOT_WHITELISTED = "SpenderWhitelist: Spender is not whitelisted"; error RuleSpenderWhitelist_InvalidTransferFrom( diff --git a/src/rules/validation/deployment/RuleBlacklist.sol b/src/rules/validation/deployment/RuleBlacklist.sol index 421abbc..f071f47 100644 --- a/src/rules/validation/deployment/RuleBlacklist.sol +++ b/src/rules/validation/deployment/RuleBlacklist.sol @@ -29,6 +29,11 @@ contract RuleBlacklist is RuleBlacklistBase, AccessControlModuleStandalone { PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view @@ -44,22 +49,40 @@ contract RuleBlacklist is RuleBlacklistBase, AccessControlModuleStandalone { ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts adding addresses to the blacklist to holders of ADDRESS_LIST_ADD_ROLE. + */ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + /** + * @notice Restricts removing addresses from the blacklist to holders of ADDRESS_LIST_REMOVE_ROLE. + */ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol b/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol index f299e82..a39eb72 100644 --- a/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; import {RuleBlacklistBase} from "../abstract/base/RuleBlacklistBase.sol"; import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; @@ -11,33 +12,76 @@ import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; * @title RuleBlacklistOwnable2Step * @notice Ownable2Step variant of RuleBlacklist with owner-based authorization hooks. */ -contract RuleBlacklistOwnable2Step is RuleBlacklistBase, Ownable2Step { +contract RuleBlacklistOwnable2Step is RuleBlacklistBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the rule and sets the initial owner and meta-transaction forwarder. + * @param owner Contract owner. + * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. + */ constructor(address owner, address forwarderIrrevocable) RuleBlacklistBase(forwarderIrrevocable) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleBlacklistBase, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleBlacklistBase.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts adding addresses to the blacklist to the contract owner. + */ function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + /** + * @notice Restricts removing addresses from the blacklist to the contract owner. + */ function _authorizeAddressListRemove() internal view virtual override onlyOwner {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleERC2980.sol b/src/rules/validation/deployment/RuleERC2980.sol index a8cbe33..488331f 100644 --- a/src/rules/validation/deployment/RuleERC2980.sol +++ b/src/rules/validation/deployment/RuleERC2980.sol @@ -37,10 +37,10 @@ contract RuleERC2980 is RuleERC2980Base, AccessControlModuleStandalone { /** * @param admin Address that receives `DEFAULT_ADMIN_ROLE` (implicitly holds all roles). * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. - * @param allowBurn If true, whitelists `address(0)` at deployment to allow burn/redemption flows. + * @param allowMintBurn When true, permits both minting and burning (sets `allowMint` and `allowBurn`). */ - constructor(address admin, address forwarderIrrevocable, bool allowBurn) - RuleERC2980Base(forwarderIrrevocable, allowBurn) + constructor(address admin, address forwarderIrrevocable, bool allowMintBurn) + RuleERC2980Base(forwarderIrrevocable, allowMintBurn) AccessControlModuleStandalone(admin) {} @@ -48,6 +48,11 @@ contract RuleERC2980 is RuleERC2980Base, AccessControlModuleStandalone { PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view @@ -62,26 +67,55 @@ contract RuleERC2980 is RuleERC2980Base, AccessControlModuleStandalone { ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts toggling `allowMint` / `allowBurn` to holders of DEFAULT_ADMIN_ROLE. + */ + function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + + /** + * @notice Restricts adding addresses to the whitelist to holders of WHITELIST_ADD_ROLE. + */ function _authorizeWhitelistAdd() internal view virtual override onlyRole(WHITELIST_ADD_ROLE) {} + /** + * @notice Restricts removing addresses from the whitelist to holders of WHITELIST_REMOVE_ROLE. + */ function _authorizeWhitelistRemove() internal view virtual override onlyRole(WHITELIST_REMOVE_ROLE) {} + /** + * @notice Restricts adding addresses to the frozenlist to holders of FROZENLIST_ADD_ROLE. + */ function _authorizeFrozenlistAdd() internal view virtual override onlyRole(FROZENLIST_ADD_ROLE) {} + /** + * @notice Restricts removing addresses from the frozenlist to holders of FROZENLIST_REMOVE_ROLE. + */ function _authorizeFrozenlistRemove() internal view virtual override onlyRole(FROZENLIST_REMOVE_ROLE) {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleERC2980Base) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleERC2980Base) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleERC2980Base) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol b/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol index 072adbe..e750766 100644 --- a/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol +++ b/src/rules/validation/deployment/RuleERC2980Ownable2Step.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; import {RuleERC2980Base} from "../abstract/base/RuleERC2980Base.sol"; /** @@ -11,7 +12,7 @@ import {RuleERC2980Base} from "../abstract/base/RuleERC2980Base.sol"; * @notice Ownable2Step variant of RuleERC2980 with owner-based authorization hooks. * @dev All whitelist and frozenlist management functions are restricted to the contract owner. */ -contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step { +contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -19,37 +20,85 @@ contract RuleERC2980Ownable2Step is RuleERC2980Base, Ownable2Step { /** * @param owner Contract owner. * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. - * @param allowBurn If true, whitelists `address(0)` at deployment to allow burn/redemption flows. + * @param allowMintBurn When true, permits both minting and burning (sets `allowMint` and `allowBurn`). */ - constructor(address owner, address forwarderIrrevocable, bool allowBurn) - RuleERC2980Base(forwarderIrrevocable, allowBurn) + constructor(address owner, address forwarderIrrevocable, bool allowMintBurn) + RuleERC2980Base(forwarderIrrevocable, allowMintBurn) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleERC2980Base, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) || RuleERC2980Base.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts toggling `allowMint` / `allowBurn` to the contract owner. + */ + function _authorizeMintBurnManager() internal view virtual override onlyOwner {} + + /** + * @notice Restricts adding addresses to the whitelist to the contract owner. + */ function _authorizeWhitelistAdd() internal view virtual override onlyOwner {} + /** + * @notice Restricts removing addresses from the whitelist to the contract owner. + */ function _authorizeWhitelistRemove() internal view virtual override onlyOwner {} + /** + * @notice Restricts adding addresses to the frozenlist to the contract owner. + */ function _authorizeFrozenlistAdd() internal view virtual override onlyOwner {} + /** + * @notice Restricts removing addresses from the frozenlist to the contract owner. + */ function _authorizeFrozenlistRemove() internal view virtual override onlyOwner {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleERC2980Base) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleERC2980Base) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleERC2980Base) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleIdentityRegistry.sol b/src/rules/validation/deployment/RuleIdentityRegistry.sol index 72109f8..4a9e887 100644 --- a/src/rules/validation/deployment/RuleIdentityRegistry.sol +++ b/src/rules/validation/deployment/RuleIdentityRegistry.sol @@ -16,15 +16,32 @@ contract RuleIdentityRegistry is AccessControlModuleStandalone, RuleIdentityRegi CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(address admin, address identityRegistry_) + /** + * @notice Deploys the rule, sets the admin and the ERC-3643 identity registry. + * @dev Pass `false, false` for the ERC-3643-conformant default: the spec requires only the + * RECEIVER to be verified. The two flags below are stricter-than-spec opt-ins. + * @param admin Address that receives the default admin role. + * @param identityRegistry_ Address of the ERC-3643 identity registry to query. + * @param checkSender_ When true, also require the sender to be verified. Stricter than + * ERC-3643, and it traps de-listed holders: a holder whose identity lapses can no + * longer exit their position. Defaults to false. + * @param checkSpender_ When true, also require the `transferFrom` spender to be verified. + * Mint and burn stay exempt from this check. Defaults to false. + */ + constructor(address admin, address identityRegistry_, bool checkSender_, bool checkSpender_) AccessControlModuleStandalone(admin) - RuleIdentityRegistryBase(identityRegistry_) + RuleIdentityRegistryBase(identityRegistry_, checkSender_, checkSpender_) {} /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view @@ -40,5 +57,8 @@ contract RuleIdentityRegistry is AccessControlModuleStandalone, RuleIdentityRegi ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts identity registry management to holders of DEFAULT_ADMIN_ROLE. + */ function _authorizeIdentityRegistryManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} } diff --git a/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol b/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol index c46ff38..d1b7883 100644 --- a/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol @@ -3,22 +3,62 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; +import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol"; import {RuleIdentityRegistryBase} from "../abstract/base/RuleIdentityRegistryBase.sol"; /** * @title RuleIdentityRegistryOwnable2Step * @notice Ownable2Step variant of RuleIdentityRegistry. */ -contract RuleIdentityRegistryOwnable2Step is RuleIdentityRegistryBase, Ownable2Step { +contract RuleIdentityRegistryOwnable2Step is RuleIdentityRegistryBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(address owner, address identityRegistry_) RuleIdentityRegistryBase(identityRegistry_) Ownable(owner) {} + /** + * @notice Deploys the rule, sets the owner and the ERC-3643 identity registry. + * @dev Pass `false, false` for the ERC-3643-conformant default: the spec requires only the + * RECEIVER to be verified. The two flags below are stricter-than-spec opt-ins. + * @param owner Contract owner. + * @param identityRegistry_ Address of the ERC-3643 identity registry to query. + * @param checkSender_ When true, also require the sender to be verified. Stricter than + * ERC-3643, and it traps de-listed holders: a holder whose identity lapses can no + * longer exit their position. Defaults to false. + * @param checkSpender_ When true, also require the `transferFrom` spender to be verified. + * Mint and burn stay exempt from this check. Defaults to false. + */ + constructor(address owner, address identityRegistry_, bool checkSender_, bool checkSpender_) + RuleIdentityRegistryBase(identityRegistry_, checkSender_, checkSpender_) + Ownable(owner) + {} + + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleTransferValidation, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleTransferValidation.supportsInterface(interfaceId); + } /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts identity registry management to the contract owner. + */ function _authorizeIdentityRegistryManager() internal view virtual override onlyOwner {} } diff --git a/src/rules/validation/deployment/RuleMaxTotalSupply.sol b/src/rules/validation/deployment/RuleMaxTotalSupply.sol index 854f45d..87c83c7 100644 --- a/src/rules/validation/deployment/RuleMaxTotalSupply.sol +++ b/src/rules/validation/deployment/RuleMaxTotalSupply.sol @@ -29,6 +29,11 @@ contract RuleMaxTotalSupply is AccessControlModuleStandalone, RuleMaxTotalSupply PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view @@ -44,5 +49,8 @@ contract RuleMaxTotalSupply is AccessControlModuleStandalone, RuleMaxTotalSupply ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts maximum total supply management to holders of DEFAULT_ADMIN_ROLE. + */ function _authorizeMaxTotalSupplyManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} } diff --git a/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol b/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol index 46a78d4..ae7e8fe 100644 --- a/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol @@ -3,25 +3,56 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; +import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol"; import {RuleMaxTotalSupplyBase} from "../abstract/base/RuleMaxTotalSupplyBase.sol"; /** * @title RuleMaxTotalSupplyOwnable2Step * @notice Ownable2Step variant of RuleMaxTotalSupply. */ -contract RuleMaxTotalSupplyOwnable2Step is RuleMaxTotalSupplyBase, Ownable2Step { +contract RuleMaxTotalSupplyOwnable2Step is RuleMaxTotalSupplyBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the rule, sets the owner, the token contract and the initial maximum supply. + * @param owner Contract owner. + * @param tokenContract_ Token contract that exposes totalSupply (must be non-zero). + * @param maxTotalSupply_ Initial maximum supply. + */ constructor(address owner, address tokenContract_, uint256 maxTotalSupply_) RuleMaxTotalSupplyBase(tokenContract_, maxTotalSupply_) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleTransferValidation, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleTransferValidation.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts maximum total supply management to the contract owner. + */ function _authorizeMaxTotalSupplyManager() internal view virtual override onlyOwner {} } diff --git a/src/rules/validation/deployment/RuleSanctionsList.sol b/src/rules/validation/deployment/RuleSanctionsList.sol index 73751c4..65558cd 100644 --- a/src/rules/validation/deployment/RuleSanctionsList.sol +++ b/src/rules/validation/deployment/RuleSanctionsList.sol @@ -21,6 +21,7 @@ contract RuleSanctionsList is AccessControlModuleStandalone, RuleSanctionsListBa /** * @param admin Address of the contract (Access Control) * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + * @param sanctionContractOracle_ Chainalysis sanctions oracle used to screen addresses */ constructor(address admin, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) AccessControlModuleStandalone(admin) @@ -31,6 +32,11 @@ contract RuleSanctionsList is AccessControlModuleStandalone, RuleSanctionsListBa PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view @@ -46,20 +52,35 @@ contract RuleSanctionsList is AccessControlModuleStandalone, RuleSanctionsListBa ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts sanctions list management to holders of SANCTIONLIST_ROLE. + */ function _authorizeSanctionListManager() internal view virtual override onlyRole(SANCTIONLIST_ROLE) {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { return ERC2771Context._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { return ERC2771Context._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { return ERC2771Context._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol b/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol index 47b40fe..85d9cbf 100644 --- a/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol @@ -4,7 +4,9 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; import {ERC2771Context} from "../../../modules/MetaTxModuleStandalone.sol"; +import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol"; import {RuleSanctionsListBase} from "../abstract/base/RuleSanctionsListBase.sol"; import {ISanctionsList} from "../../interfaces/ISanctionsList.sol"; @@ -12,34 +14,75 @@ import {ISanctionsList} from "../../interfaces/ISanctionsList.sol"; * @title RuleSanctionsListOwnable2Step * @notice Ownable2Step variant of RuleSanctionsList. */ -contract RuleSanctionsListOwnable2Step is RuleSanctionsListBase, Ownable2Step { +contract RuleSanctionsListOwnable2Step is RuleSanctionsListBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the rule, sets the owner, the forwarder and the sanctions oracle. + * @param owner Contract owner. + * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. + * @param sanctionContractOracle_ Chainalysis sanctions oracle used to screen addresses. + */ constructor(address owner, address forwarderIrrevocable, ISanctionsList sanctionContractOracle_) RuleSanctionsListBase(forwarderIrrevocable, sanctionContractOracle_) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleTransferValidation, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleTransferValidation.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts sanctions list management to the contract owner. + */ function _authorizeSanctionListManager() internal view virtual override onlyOwner {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(ERC2771Context, Context) returns (address sender) { return ERC2771Context._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(ERC2771Context, Context) returns (bytes calldata) { return ERC2771Context._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { return ERC2771Context._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleSpenderWhitelist.sol b/src/rules/validation/deployment/RuleSpenderWhitelist.sol index 50d3bf3..e0c3607 100644 --- a/src/rules/validation/deployment/RuleSpenderWhitelist.sol +++ b/src/rules/validation/deployment/RuleSpenderWhitelist.sol @@ -6,7 +6,6 @@ import {Context} from "@openzeppelin/contracts/utils/Context.sol"; import {AccessControlModuleStandalone} from "../../../modules/AccessControlModuleStandalone.sol"; import {RuleSpenderWhitelistBase} from "../abstract/base/RuleSpenderWhitelistBase.sol"; import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; -import {RuleTransferValidation} from "../abstract/core/RuleTransferValidation.sol"; /** * @title RuleSpenderWhitelist @@ -17,6 +16,11 @@ contract RuleSpenderWhitelist is RuleSpenderWhitelistBase, AccessControlModuleSt CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the rule, sets the admin and the meta-transaction forwarder. + * @param admin Address that receives the default admin role. + * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. + */ constructor(address admin, address forwarderIrrevocable) RuleSpenderWhitelistBase(forwarderIrrevocable) AccessControlModuleStandalone(admin) @@ -26,37 +30,60 @@ contract RuleSpenderWhitelist is RuleSpenderWhitelistBase, AccessControlModuleSt PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view virtual - override(AccessControlEnumerable, RuleTransferValidation) + override(AccessControlEnumerable, RuleSpenderWhitelistBase) returns (bool) { return AccessControlEnumerable.supportsInterface(interfaceId) - || RuleTransferValidation.supportsInterface(interfaceId); + || RuleSpenderWhitelistBase.supportsInterface(interfaceId); } /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts adding addresses to the spender whitelist to holders of ADDRESS_LIST_ADD_ROLE. + */ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + /** + * @notice Restricts removing addresses from the spender whitelist to holders of ADDRESS_LIST_REMOVE_ROLE. + */ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol b/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol index f9c2de8..e3fd758 100644 --- a/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; import {RuleSpenderWhitelistBase} from "../abstract/base/RuleSpenderWhitelistBase.sol"; import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; @@ -11,36 +12,79 @@ import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; * @title RuleSpenderWhitelistOwnable2Step * @notice Ownable2Step deployment variant of spender whitelist rule. */ -contract RuleSpenderWhitelistOwnable2Step is RuleSpenderWhitelistBase, Ownable2Step { +contract RuleSpenderWhitelistOwnable2Step is RuleSpenderWhitelistBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + /** + * @notice Deploys the rule, sets the owner and the meta-transaction forwarder. + * @param owner Contract owner. + * @param forwarderIrrevocable Address of the ERC-2771 forwarder for meta-transactions. + */ constructor(address owner, address forwarderIrrevocable) RuleSpenderWhitelistBase(forwarderIrrevocable) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleSpenderWhitelistBase, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleSpenderWhitelistBase.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts adding addresses to the spender whitelist to the contract owner. + */ function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + /** + * @notice Restricts removing addresses from the spender whitelist to the contract owner. + */ function _authorizeAddressListRemove() internal view virtual override onlyOwner {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleWhitelist.sol b/src/rules/validation/deployment/RuleWhitelist.sol index 4168280..3b9bac3 100644 --- a/src/rules/validation/deployment/RuleWhitelist.sol +++ b/src/rules/validation/deployment/RuleWhitelist.sol @@ -59,24 +59,50 @@ contract RuleWhitelist is RuleWhitelistBase, AccessControlModuleStandalone { ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts toggling the spender-check setting to holders of DEFAULT_ADMIN_ROLE. + */ function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + /** + * @notice Restricts toggling `allowMint` / `allowBurn` to holders of DEFAULT_ADMIN_ROLE. + */ + function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + + /** + * @notice Restricts adding addresses to the whitelist to holders of ADDRESS_LIST_ADD_ROLE. + */ function _authorizeAddressListAdd() internal view virtual override onlyRole(ADDRESS_LIST_ADD_ROLE) {} + /** + * @notice Restricts removing addresses from the whitelist to holders of ADDRESS_LIST_REMOVE_ROLE. + */ function _authorizeAddressListRemove() internal view virtual override onlyRole(ADDRESS_LIST_REMOVE_ROLE) {} /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol b/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol index d2f8aaf..f235e7b 100644 --- a/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; import {RuleWhitelistBase} from "../abstract/base/RuleWhitelistBase.sol"; import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; @@ -11,7 +12,7 @@ import {RuleAddressSet} from "../abstract/RuleAddressSet/RuleAddressSet.sol"; * @title RuleWhitelistOwnable2Step * @notice Ownable2Step variant of RuleWhitelist with owner-based authorization hooks. */ -contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step { +contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -27,28 +28,74 @@ contract RuleWhitelistOwnable2Step is RuleWhitelistBase, Ownable2Step { Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleWhitelistBase, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleWhitelistBase.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts adding addresses to the whitelist to the contract owner. + */ function _authorizeAddressListAdd() internal view virtual override onlyOwner {} + /** + * @notice Restricts removing addresses from the whitelist to the contract owner. + */ function _authorizeAddressListRemove() internal view virtual override onlyOwner {} + /** + * @notice Restricts toggling the spender-check setting to the contract owner. + */ function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} + /** + * @notice Restricts toggling `allowMint` / `allowBurn` to the contract owner. + */ + function _authorizeMintBurnManager() internal view virtual override onlyOwner {} + /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(Context, RuleAddressSet) returns (address sender) { return super._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(Context, RuleAddressSet) returns (bytes calldata) { return super._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view virtual override(Context, RuleAddressSet) returns (uint256) { return super._contextSuffixLength(); } diff --git a/src/rules/validation/deployment/RuleWhitelistWrapper.sol b/src/rules/validation/deployment/RuleWhitelistWrapper.sol index 99297e1..b4cc5ba 100644 --- a/src/rules/validation/deployment/RuleWhitelistWrapper.sol +++ b/src/rules/validation/deployment/RuleWhitelistWrapper.sol @@ -8,20 +8,28 @@ import {Context} from "@openzeppelin/contracts/utils/Context.sol"; /* ==== Abstract contracts === */ import {AccessControlModuleStandalone} from "../../../modules/AccessControlModuleStandalone.sol"; import {RuleWhitelistWrapperBase} from "../abstract/base/RuleWhitelistWrapperBase.sol"; +/* ==== RuleEngine === */ +import {RulesManagementModuleRolesStorage} from "RuleEngine/modules/library/RulesManagementModuleRolesStorage.sol"; /** * @title Wrapper to call several different whitelist rules */ -contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleStandalone { +contract RuleWhitelistWrapper is + RuleWhitelistWrapperBase, + AccessControlModuleStandalone, + RulesManagementModuleRolesStorage +{ /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ /** * @param admin Address of the contract (Access Control) * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + * @param checkSpender_ Enables spender checks for transferFrom when true. + * @param allowMintBurn When true, permits both minting and burning (sets `allowMint` and `allowBurn`). */ - constructor(address admin, address forwarderIrrevocable, bool checkSpender_) - RuleWhitelistWrapperBase(forwarderIrrevocable, checkSpender_) + constructor(address admin, address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) + RuleWhitelistWrapperBase(forwarderIrrevocable, checkSpender_, allowMintBurn) AccessControlModuleStandalone(admin) {} @@ -30,18 +38,21 @@ contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleSt //////////////////////////////////////////////////////////////*/ /** + * @notice Returns whether `account` has been granted `role`. * @dev Returns `true` if `account` has been granted `role`. + * @param role Role identifier being queried. + * @param account Address being checked for the role. + * @return True if `account` holds `role`. */ - function hasRole(bytes32 role, address account) - public - view - virtual - override - returns (bool) - { + function hasRole(bytes32 role, address account) public view virtual override returns (bool) { return AccessControlModuleStandalone.hasRole(role, account); } + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ function supportsInterface(bytes4 interfaceId) public view @@ -54,28 +65,74 @@ contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleSt } /*////////////////////////////////////////////////////////////// - ACCESS CONTROL + INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ - function _authorizeCheckSpenderManager() internal virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + /** + * @notice Grants `role` to `account`, keeping role enumeration in sync. + * @param role Role identifier to grant. + * @param account Address receiving the role. + * @return True if the role was newly granted. + */ + function _grantRole(bytes32 role, address account) internal virtual override returns (bool) { + return AccessControlEnumerable._grantRole(role, account); + } /** - * @dev Restrict rules management to the dedicated role. + * @notice Revokes `role` from `account`, keeping role enumeration in sync. + * @param role Role identifier to revoke. + * @param account Address losing the role. + * @return True if the role was previously held and is now revoked. */ - function _onlyRulesManager() internal virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) { + return AccessControlEnumerable._revokeRole(role, account); + } /*////////////////////////////////////////////////////////////// - INTERNAL FUNCTIONS + ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts toggling the spender-check setting to holders of DEFAULT_ADMIN_ROLE. + */ + function _authorizeCheckSpenderManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + + /** + * @notice Restricts toggling `allowMint` / `allowBurn` to holders of DEFAULT_ADMIN_ROLE. + */ + function _authorizeMintBurnManager() internal view virtual override onlyRole(DEFAULT_ADMIN_ROLE) {} + + /** + * @notice Restricts rules management to holders of RULES_MANAGEMENT_ROLE. + * @dev Restrict rules management to the dedicated role. + */ + function _onlyRulesManager() internal view virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + + /** + * @notice Restricts rules-limit management to holders of RULES_MANAGEMENT_ROLE. + */ + function _onlyRulesLimitManager() internal view virtual override onlyRole(RULES_MANAGEMENT_ROLE) {} + + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (address sender) { return RuleWhitelistWrapperBase._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (bytes calldata) { return RuleWhitelistWrapperBase._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view @@ -85,22 +142,4 @@ contract RuleWhitelistWrapper is RuleWhitelistWrapperBase, AccessControlModuleSt { return RuleWhitelistWrapperBase._contextSuffixLength(); } - - function _grantRole(bytes32 role, address account) - internal - virtual - override - returns (bool) - { - return AccessControlEnumerable._grantRole(role, account); - } - - function _revokeRole(bytes32 role, address account) - internal - virtual - override - returns (bool) - { - return AccessControlEnumerable._revokeRole(role, account); - } } diff --git a/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol b/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol index d5f7bad..7b111f3 100644 --- a/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol +++ b/src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol @@ -6,48 +6,97 @@ pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {Ownable2StepERC165Module} from "../../../modules/Ownable2StepERC165Module.sol"; /* ==== Abstract contracts === */ import {RuleWhitelistWrapperBase} from "../abstract/base/RuleWhitelistWrapperBase.sol"; /** * @title Wrapper to call several different whitelist rules (Ownable2Step) */ -contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2Step { +contract RuleWhitelistWrapperOwnable2Step is RuleWhitelistWrapperBase, Ownable2Step, Ownable2StepERC165Module { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ /** * @param owner Address of the contract owner * @param forwarderIrrevocable Address of the forwarder, required for the gasless support + * @param checkSpender_ Enables spender checks for transferFrom when true. + * @param allowMintBurn When true, permits both minting and burning (sets `allowMint` and `allowBurn`). */ - constructor(address owner, address forwarderIrrevocable, bool checkSpender_) - RuleWhitelistWrapperBase(forwarderIrrevocable, checkSpender_) + constructor(address owner, address forwarderIrrevocable, bool checkSpender_, bool allowMintBurn) + RuleWhitelistWrapperBase(forwarderIrrevocable, checkSpender_, allowMintBurn) Ownable(owner) {} + /*////////////////////////////////////////////////////////////// + PUBLIC FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Indicates whether this contract supports a given interface. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the interface is supported. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(RuleWhitelistWrapperBase, Ownable2StepERC165Module) + returns (bool) + { + return Ownable2StepERC165Module.supportsInterface(interfaceId) + || RuleWhitelistWrapperBase.supportsInterface(interfaceId); + } + /*////////////////////////////////////////////////////////////// ACCESS CONTROL //////////////////////////////////////////////////////////////*/ + /** + * @notice Restricts toggling the spender-check setting to the contract owner. + */ function _authorizeCheckSpenderManager() internal view virtual override onlyOwner {} /** + * @notice Restricts toggling `allowMint` / `allowBurn` to the contract owner. + */ + function _authorizeMintBurnManager() internal view virtual override onlyOwner {} + + /** + * @notice Restricts rules management to the contract owner. * @dev Restrict rules management to the owner. */ function _onlyRulesManager() internal view virtual override onlyOwner {} + /** + * @notice Restricts rules-limit management to the contract owner. + */ + function _onlyRulesLimitManager() internal view virtual override onlyOwner {} + /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ + /** + * @notice Returns the message sender, accounting for meta-transaction (ERC-2771) context. + * @return sender The address of the message sender. + */ function _msgSender() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (address sender) { return RuleWhitelistWrapperBase._msgSender(); } + /** + * @notice Returns the message calldata, accounting for meta-transaction (ERC-2771) context. + * @return The message calldata. + */ function _msgData() internal view virtual override(RuleWhitelistWrapperBase, Context) returns (bytes calldata) { return RuleWhitelistWrapperBase._msgData(); } + /** + * @notice Returns the length of the context suffix appended by the forwarder. + * @return The context suffix length in bytes. + */ function _contextSuffixLength() internal view diff --git a/test/Coverage/DeploymentOperationCoverage.t.sol b/test/Coverage/DeploymentOperationCoverage.t.sol index 8eb1297..410a15f 100644 --- a/test/Coverage/DeploymentOperationCoverage.t.sol +++ b/test/Coverage/DeploymentOperationCoverage.t.sol @@ -30,7 +30,8 @@ contract DeploymentCoverageExtraTest is Test, HelperContract { function testDeploymentWrappersAndHooksCoverage() public { RuleBlacklistHarness blacklist = new RuleBlacklistHarness(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS); RuleWhitelistHarness whitelist = new RuleWhitelistHarness(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, false); - RuleWhitelistWrapperHarness wrapper = new RuleWhitelistWrapperHarness(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true); + RuleWhitelistWrapperHarness wrapper = + new RuleWhitelistWrapperHarness(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, true); RuleERC2980Harness erc2980 = new RuleERC2980Harness(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, false); RuleSanctionsListHarness sanctions = new RuleSanctionsListHarness(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ISanctionsList(ZERO_ADDRESS)); @@ -40,7 +41,7 @@ contract DeploymentCoverageExtraTest is Test, HelperContract { RuleWhitelistOwnable2StepHarness whitelistOwnable = new RuleWhitelistOwnable2StepHarness(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, false); RuleWhitelistWrapperOwnable2StepHarness wrapperOwnable = - new RuleWhitelistWrapperOwnable2StepHarness(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true); + new RuleWhitelistWrapperOwnable2StepHarness(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, true); RuleERC2980Ownable2StepHarness erc2980Ownable = new RuleERC2980Ownable2StepHarness(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, false); diff --git a/test/DeploymentScripts/DeployCMTATWithBlacklist.t.sol b/test/DeploymentScripts/DeployCMTATWithBlacklist.t.sol index 30a0cb5..92a25ca 100644 --- a/test/DeploymentScripts/DeployCMTATWithBlacklist.t.sol +++ b/test/DeploymentScripts/DeployCMTATWithBlacklist.t.sol @@ -2,19 +2,22 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; -import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol"; import {DeployCMTATWithBlacklist} from "script/DeployCMTATWithBlacklist.s.sol"; contract DeployCMTATWithBlacklistTest is Test { function testDeployCMTATWithBlacklist() public { DeployCMTATWithBlacklist script = new DeployCMTATWithBlacklist(); - (CMTATStandalone token, RuleBlacklist rule) = _deploy(script); + (CMTATStandardStandalone token, RuleBlacklist rule) = _deploy(script); assertEq(address(token.ruleEngine()), address(rule)); } - function _deploy(DeployCMTATWithBlacklist script) internal returns (CMTATStandalone token, RuleBlacklist rule) { + function _deploy(DeployCMTATWithBlacklist script) + internal + returns (CMTATStandardStandalone token, RuleBlacklist rule) + { (token, rule) = script.deploy(address(1), address(0)); } } diff --git a/test/DeploymentScripts/DeployCMTATWithBlacklistAndSanctionsList.t.sol b/test/DeploymentScripts/DeployCMTATWithBlacklistAndSanctionsList.t.sol index 35d1f9a..e269aa0 100644 --- a/test/DeploymentScripts/DeployCMTATWithBlacklistAndSanctionsList.t.sol +++ b/test/DeploymentScripts/DeployCMTATWithBlacklistAndSanctionsList.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; -import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol"; import {RuleSanctionsList} from "src/rules/validation/deployment/RuleSanctionsList.sol"; @@ -39,7 +39,7 @@ contract DeployCMTATWithBlacklistAndSanctionsListTest is uint8 constant TRANSFER_OK = 0; uint256 constant INITIAL_BALANCE = 100; - CMTATStandalone token; + CMTATStandardStandalone token; RuleEngine ruleEngine; RuleBlacklist ruleBlacklist; RuleSanctionsList ruleSanctionsList; @@ -255,8 +255,9 @@ contract DeployCMTATWithBlacklistAndSanctionsListTest is vm.expectRevert( abi.encodeWithSelector( - RuleBlacklist_InvalidTransfer.selector, + RuleBlacklist_InvalidTransferFrom.selector, address(ruleBlacklist), + ADMIN, address(0), ADDRESS3, amount, diff --git a/test/DeploymentScripts/DeployCMTATWithWhitelist.t.sol b/test/DeploymentScripts/DeployCMTATWithWhitelist.t.sol index ded0513..490e8d0 100644 --- a/test/DeploymentScripts/DeployCMTATWithWhitelist.t.sol +++ b/test/DeploymentScripts/DeployCMTATWithWhitelist.t.sol @@ -2,18 +2,21 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; -import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; import {DeployCMTATWithWhitelist} from "script/DeployCMTATWithWhitelist.s.sol"; contract DeployCMTATWithWhitelistTest is Test { function testDeployCMTATWithWhitelist() public { DeployCMTATWithWhitelist script = new DeployCMTATWithWhitelist(); - (CMTATStandalone token, RuleWhitelist rule) = _deploy(script); + (CMTATStandardStandalone token, RuleWhitelist rule) = _deploy(script); assertEq(address(token.ruleEngine()), address(rule)); } - function _deploy(DeployCMTATWithWhitelist script) internal returns (CMTATStandalone token, RuleWhitelist rule) { + function _deploy(DeployCMTATWithWhitelist script) + internal + returns (CMTATStandardStandalone token, RuleWhitelist rule) + { (token, rule) = script.deploy(address(1), address(0), false); } } diff --git a/test/HelperContract.sol b/test/HelperContract.sol index d7a11a0..f833da5 100644 --- a/test/HelperContract.sol +++ b/test/HelperContract.sol @@ -1,7 +1,7 @@ //SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.20; -import {CMTATStandalone} from "CMTAT/deployment/CMTATStandalone.sol"; +import {CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; // RuleEngine import {RuleEngineInvariantStorage} from "RuleEngine/modules/library/RuleEngineInvariantStorage.sol"; @@ -21,6 +21,8 @@ import {RuleConditionalTransferLight} from "src/rules/operation/RuleConditionalT import { RuleConditionalTransferLightInvariantStorage } from "src/rules/operation/abstract/RuleConditionalTransferLightInvariantStorage.sol"; +// RuleMintAllowance +import {RuleMintAllowanceInvariantStorage} from "src/rules/operation/abstract/RuleMintAllowanceInvariantStorage.sol"; import { RuleWhitelistInvariantStorage } from "src/rules/validation/abstract/RuleAddressSet/invariantStorage/RuleWhitelistInvariantStorage.sol"; @@ -39,7 +41,7 @@ import { } from "src/rules/validation/abstract/invariant/RuleSanctionsListInvariantStorage.sol"; // utils -import {CMTATDeployment} from "RuleEngine/../test/utils/CMTATDeployment.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; /** * @title Constants used by the tests @@ -52,6 +54,7 @@ abstract contract HelperContract is RuleMaxTotalSupplyInvariantStorage, RuleIdentityRegistryInvariantStorage, RuleConditionalTransferLightInvariantStorage, + RuleMintAllowanceInvariantStorage, RuleEngineInvariantStorage { // Test result @@ -87,7 +90,7 @@ abstract contract HelperContract is // CMTAT CMTATDeployment cmtatDeployment; - CMTATStandalone internal cmtatContract; + CMTATStandardStandalone internal cmtatContract; // RuleEngine Mock RuleEngine public ruleEngineMock; diff --git a/test/InterfaceId/AddressListInterfaceId.t.sol b/test/InterfaceId/AddressListInterfaceId.t.sol new file mode 100644 index 0000000..92ba907 --- /dev/null +++ b/test/InterfaceId/AddressListInterfaceId.t.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; + +import {AddressListInterfaceId} from "src/rules/interfaces/library/AddressListInterfaceId.sol"; +import {IAddressListInterfaceIdHelper, IAddressListAllFunctions} from "src/mocks/IAddressListInterfaceIdHelper.sol"; +import {IIdentityRegistryContains} from "src/rules/interfaces/IIdentityRegistry.sol"; + +import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; +import {RuleWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol"; +import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol"; +import {RuleBlacklistOwnable2Step} from "src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol"; +import {RuleSpenderWhitelist} from "src/rules/validation/deployment/RuleSpenderWhitelist.sol"; +import {RuleSpenderWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol"; +import {RuleWhitelistWrapper} from "src/rules/validation/deployment/RuleWhitelistWrapper.sol"; +import {RuleMaxTotalSupply} from "src/rules/validation/deployment/RuleMaxTotalSupply.sol"; +import {TotalSupplyMock} from "src/mocks/TotalSupplyMock.sol"; + +/** + * @title AddressListInterfaceIdTest + * @notice Verifies the pre-computed {AddressListInterfaceId} constant and its advertisement. + * @dev First step of improvement I-4: every rule that implements {IAddressList} must advertise it + * via ERC-165, so that `RuleWhitelistWrapper` can later interface-check its child rules + * (threat `WW-2`, finding F-5). + */ +contract AddressListInterfaceIdTest is Test, HelperContract { + address private constant FORWARDER = address(0); + + IAddressListInterfaceIdHelper private helper; + + function setUp() public { + helper = new IAddressListInterfaceIdHelper(); + } + + /*////////////////////////////////////////////////////////////// + THE CONSTANT ITSELF + //////////////////////////////////////////////////////////////*/ + + /** + * @notice The library constant equals the XOR of every selector in the IAddressList hierarchy. + */ + function test_ConstantMatchesFlattenedInterfaceId() public view { + assertEq( + AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID, + type(IAddressListAllFunctions).interfaceId, + "constant does not match the flattened hierarchy" + ); + assertEq(AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID, bytes4(0x5d10e182)); + } + + /** + * @notice Guards the reason the flat-helper pattern is required: `type(IAddressList).interfaceId` + * omits `contains(address)`, inherited from `IIdentityRegistryContains`, so it must NOT + * be used for the ERC-165 check. + */ + function test_NaiveInterfaceIdIsWrongAndMustNotBeUsed() public view { + bytes4 naive = helper.getIAddressListInterfaceId(); + bytes4 full = AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID; + + assertTrue(naive != full, "naive id unexpectedly equals the full id"); + // The difference is exactly the inherited selector, `contains(address)`. + assertEq(naive ^ full, helper.getIIdentityRegistryContainsInterfaceId()); + assertEq(naive ^ full, IIdentityRegistryContains.contains.selector); + } + + /*////////////////////////////////////////////////////////////// + RULES THAT MUST ADVERTISE IT + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Every rule backed by an address set advertises IAddressList (AccessControl variants). + */ + function test_AddressSetRulesAdvertiseIAddressList() public { + bytes4 id = AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID; + + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + assertTrue( + new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false).supportsInterface(id), "RuleWhitelist" + ); + assertTrue(new RuleBlacklist(DEFAULT_ADMIN_ADDRESS, FORWARDER).supportsInterface(id), "RuleBlacklist"); + assertTrue( + new RuleSpenderWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER).supportsInterface(id), "RuleSpenderWhitelist" + ); + vm.stopPrank(); + } + + /** + * @notice Same for the Ownable2Step variants — the advertisement lives in the shared base, + * so both access-control flavours must agree. + */ + function test_AddressSetRulesOwnable2StepAdvertiseIAddressList() public { + bytes4 id = AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID; + + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + assertTrue( + new RuleWhitelistOwnable2Step(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false).supportsInterface(id), + "RuleWhitelistOwnable2Step" + ); + assertTrue( + new RuleBlacklistOwnable2Step(DEFAULT_ADMIN_ADDRESS, FORWARDER).supportsInterface(id), + "RuleBlacklistOwnable2Step" + ); + assertTrue( + new RuleSpenderWhitelistOwnable2Step(DEFAULT_ADMIN_ADDRESS, FORWARDER).supportsInterface(id), + "RuleSpenderWhitelistOwnable2Step" + ); + vm.stopPrank(); + } + + /** + * @notice Advertising IAddressList must not break the existing ERC-165 advertisements. + */ + function test_AdvertisementDoesNotBreakExistingInterfaces() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelist rule = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false); + + assertTrue(rule.supportsInterface(type(IERC165).interfaceId), "IERC165"); + assertTrue(rule.supportsInterface(RuleInterfaceId.IRULE_INTERFACE_ID), "IRule"); + assertFalse(rule.supportsInterface(bytes4(0xdeadbeef)), "unknown interface"); + } + + /*////////////////////////////////////////////////////////////// + RULES THAT MUST *NOT* ADVERTISE IT + //////////////////////////////////////////////////////////////*/ + + /** + * @notice A rule with no address set must not claim IAddressList — this is precisely the case + * the wrapper's future `_checkRule` guard (I-4) needs to reject. + */ + function test_NonAddressListRuleDoesNotAdvertiseIt() public { + TotalSupplyMock token = new TotalSupplyMock(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleMaxTotalSupply rule = new RuleMaxTotalSupply(DEFAULT_ADMIN_ADDRESS, address(token), 1000); + + assertFalse( + rule.supportsInterface(AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID), + "RuleMaxTotalSupply must not claim IAddressList" + ); + } + + /** + * @notice `RuleWhitelistWrapper` aggregates child address lists but exposes no address set of its + * own (no `addAddress`/`areAddressesListed`), so it correctly does NOT advertise + * IAddressList — meaning a wrapper cannot be nested as a child of another wrapper. + */ + function test_WrapperDoesNotAdvertiseIAddressList() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelistWrapper wrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + + assertFalse( + wrapper.supportsInterface(AddressListInterfaceId.IADDRESS_LIST_INTERFACE_ID), + "wrapper is not itself an address list" + ); + } +} diff --git a/test/MintBurnFlags/MintBurnFlags.t.sol b/test/MintBurnFlags/MintBurnFlags.t.sol new file mode 100644 index 0000000..deb2b1b --- /dev/null +++ b/test/MintBurnFlags/MintBurnFlags.t.sol @@ -0,0 +1,379 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {IRule} from "RuleEngine/interfaces/IRule.sol"; + +import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; +import {RuleWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol"; +import {RuleWhitelistWrapper} from "src/rules/validation/deployment/RuleWhitelistWrapper.sol"; +import {RuleWhitelistWrapperOwnable2Step} from "src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol"; +import {RuleERC2980} from "src/rules/validation/deployment/RuleERC2980.sol"; +import {RuleERC2980Ownable2Step} from "src/rules/validation/deployment/RuleERC2980Ownable2Step.sol"; + +/** + * @title MintBurnFlags + * @notice Covers the explicit `allowMint` / `allowBurn` flags (improvement I-12) across ALL SIX + * deployments that carry them, including the access-control hook on each variant. + * @dev The flags replaced the old "whitelist `address(0)` to enable mint/burn" idiom, which made the + * standardized getters assert falsehoods (`isVerified(0)` per ERC-3643; `whitelist(0)` per + * ERC-2980, a MANDATORY getter). The invariant these tests defend is: + * + * mint/burn can be enabled WITHOUT the zero address ever entering a list. + */ +contract MintBurnFlags is Test, HelperContract { + address private constant FORWARDER = address(0); + address private constant OWNER = address(11); + + // ERC-2980 codes (local aliases: the whitelist family's 24/25 are inherited via HelperContract). + uint8 private constant CODE_ERC2980_MINT_NOT_ALLOWED = 64; + uint8 private constant CODE_ERC2980_BURN_NOT_ALLOWED = 65; + + /*////////////////////////////////////////////////////////////// + THE INVARIANT THAT MATTERS + //////////////////////////////////////////////////////////////*/ + + /// @notice With mint/burn ENABLED, every standardized identity getter must still be truthful. + function test_MintBurnEnabled_StandardizedGettersStayTruthful() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelist w = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + RuleERC2980 e = new RuleERC2980(DEFAULT_ADMIN_ADDRESS, FORWARDER, true); + vm.stopPrank(); + + assertTrue(w.allowMint() && w.allowBurn()); + assertTrue(e.allowMint() && e.allowBurn()); + + // ERC-3643: `isVerified` means "a valid investor holding the required claims". + assertFalse(w.isVerified(ZERO_ADDRESS)); + assertFalse(w.contains(ZERO_ADDRESS)); + assertFalse(w.isAddressListed(ZERO_ADDRESS)); + assertEq(w.listedAddressCount(), 0); + + // ERC-2980: `whitelist` / `frozenlist` are MANDATORY getters. + assertFalse(e.whitelist(ZERO_ADDRESS)); + assertFalse(e.frozenlist(ZERO_ADDRESS)); + assertFalse(e.isVerified(ZERO_ADDRESS)); + } + + /*////////////////////////////////////////////////////////////// + RESTRICTION MESSAGES FOR THE NEW MINT/BURN CODES + + A restriction code is only useful if `messageForTransferRestriction` + can explain it. The four codes added with the flags (24/25 on the + whitelist family, 64/65 on ERC-2980) each need a message — otherwise + a rejected mint reports "Unknown code", which is exactly the opaque + failure the dedicated codes were introduced to remove. + //////////////////////////////////////////////////////////////*/ + + function test_Messages_WhitelistFamilyExplainsMintAndBurnCodes() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelist w = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false); + RuleWhitelistWrapper wr = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false); + vm.stopPrank(); + + assertEq(w.messageForTransferRestriction(CODE_MINT_NOT_ALLOWED), TEXT_MINT_NOT_ALLOWED); + assertEq(w.messageForTransferRestriction(CODE_BURN_NOT_ALLOWED), TEXT_BURN_NOT_ALLOWED); + + // The wrapper shares `RuleWhitelistShared`, so it must answer identically. + assertEq(wr.messageForTransferRestriction(CODE_MINT_NOT_ALLOWED), TEXT_MINT_NOT_ALLOWED); + assertEq(wr.messageForTransferRestriction(CODE_BURN_NOT_ALLOWED), TEXT_BURN_NOT_ALLOWED); + } + + function test_Messages_ERC2980ExplainsMintAndBurnCodes() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleERC2980 e = new RuleERC2980(DEFAULT_ADMIN_ADDRESS, FORWARDER, false); + + assertEq(e.messageForTransferRestriction(CODE_ERC2980_MINT_NOT_ALLOWED), "Minting is not allowed"); + assertEq(e.messageForTransferRestriction(CODE_ERC2980_BURN_NOT_ALLOWED), "Burning is not allowed"); + } + + /** + * @notice The message must be reachable from the code a REAL rejected mint returns. + * @dev Guards the pairing, not just the lookup: a test that asserts the message table in + * isolation still passes if `_detectMintBurnRestriction` returns the wrong code. + */ + function test_Messages_RejectedMintCodeMapsToItsOwnMessage() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelist w = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false); + + uint8 code = w.detectTransferRestriction(ZERO_ADDRESS, address(0x1234), 100); + assertEq(code, CODE_MINT_NOT_ALLOWED); + assertEq(w.messageForTransferRestriction(code), TEXT_MINT_NOT_ALLOWED); + } + + /*////////////////////////////////////////////////////////////// + DEGENERATE (from == 0 && to == 0): WRAPPER / WHITELIST PARITY + //////////////////////////////////////////////////////////////*/ + + /** + * @notice A (0, 0) movement is neither a mint nor a burn of anything real — nothing to screen. + * @dev `RuleWhitelistWrapperBase` handles this case explicitly so that it cannot drift from + * `RuleWhitelistBase`. The wrapper owns no addresses, so without the explicit branch it + * would try to resolve `address(0)` against its children and reject. The two must agree, + * and with mint+burn BOTH enabled the agreed answer is TRANSFER_OK. + */ + function test_Degenerate_ZeroToZero_WrapperAgreesWithWhitelist() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelist w = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + RuleWhitelistWrapper wr = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + IRule[] memory rules = new IRule[](1); + rules[0] = IRule(address(w)); + wr.setRules(rules); + vm.stopPrank(); + + assertEq(wr.detectTransferRestriction(ZERO_ADDRESS, ZERO_ADDRESS, 100), NO_ERROR); + assertEq( + wr.detectTransferRestriction(ZERO_ADDRESS, ZERO_ADDRESS, 100), + w.detectTransferRestriction(ZERO_ADDRESS, ZERO_ADDRESS, 100), + "wrapper and whitelist must not drift on the degenerate case" + ); + } + + /// @notice ...and when mint is disabled, the degenerate case is gated by the mint flag first. + function test_Degenerate_ZeroToZero_GatedByMintFlag() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelistWrapper wr = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false); + RuleWhitelist w = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false); + IRule[] memory rules = new IRule[](1); + rules[0] = IRule(address(w)); + wr.setRules(rules); + vm.stopPrank(); + + assertEq(wr.detectTransferRestriction(ZERO_ADDRESS, ZERO_ADDRESS, 100), CODE_MINT_NOT_ALLOWED); + assertEq( + wr.detectTransferRestriction(ZERO_ADDRESS, ZERO_ADDRESS, 100), + w.detectTransferRestriction(ZERO_ADDRESS, ZERO_ADDRESS, 100) + ); + } + + /*////////////////////////////////////////////////////////////// + RuleWhitelist (both variants) + //////////////////////////////////////////////////////////////*/ + + function test_Whitelist_SettersToggleBothFlagsIndependently() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelist w = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + w.addAddress(ADDRESS1); + + // Close issuance, keep redemptions open. + w.setAllowMint(false); + assertFalse(w.allowMint()); + assertTrue(w.allowBurn()); + assertEq(w.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, 10), CODE_MINT_NOT_ALLOWED); + assertEq(w.detectTransferRestriction(ADDRESS1, ZERO_ADDRESS, 10), TRANSFER_OK); + + // Now also close redemptions. + w.setAllowBurn(false); + assertFalse(w.allowBurn()); + assertEq(w.detectTransferRestriction(ADDRESS1, ZERO_ADDRESS, 10), CODE_BURN_NOT_ALLOWED); + + // Re-open both. + w.setAllowMint(true); + w.setAllowBurn(true); + vm.stopPrank(); + + assertEq(w.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, 10), TRANSFER_OK); + assertEq(w.detectTransferRestriction(ADDRESS1, ZERO_ADDRESS, 10), TRANSFER_OK); + } + + function test_Whitelist_SettersAreAdminOnly() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelist w = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + + vm.prank(ATTACKER); + vm.expectRevert(); + w.setAllowMint(false); + + vm.prank(ATTACKER); + vm.expectRevert(); + w.setAllowBurn(false); + + assertTrue(w.allowMint() && w.allowBurn()); + } + + function test_WhitelistOwnable2Step_SettersAreOwnerOnly() public { + RuleWhitelistOwnable2Step w = new RuleWhitelistOwnable2Step(OWNER, FORWARDER, false, false); + assertFalse(w.allowMint()); + assertFalse(w.allowBurn()); + + vm.prank(ATTACKER); + vm.expectRevert(); + w.setAllowMint(true); + + vm.startPrank(OWNER); + w.setAllowMint(true); + w.setAllowBurn(true); + vm.stopPrank(); + + assertTrue(w.allowMint() && w.allowBurn()); + assertFalse(w.isVerified(ZERO_ADDRESS)); + } + + /*////////////////////////////////////////////////////////////// + RuleWhitelistWrapper (both variants) + //////////////////////////////////////////////////////////////*/ + + function test_Wrapper_MintRequiresRecipientListedInAChild() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelist child = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + child.addAddress(ADDRESS1); + + RuleWhitelistWrapper wrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + wrapper.addRule(IRule(address(child))); + vm.stopPrank(); + + // Mint to a listed recipient: allowed. The flag permits the OPERATION; the recipient is + // still screened. + assertEq(wrapper.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, 10), TRANSFER_OK); + // Mint to an UNLISTED recipient: still rejected. + assertEq(wrapper.detectTransferRestriction(ZERO_ADDRESS, ATTACKER, 10), CODE_ADDRESS_TO_NOT_WHITELISTED); + // Burn from a listed sender: allowed; from an unlisted sender: rejected. + assertEq(wrapper.detectTransferRestriction(ADDRESS1, ZERO_ADDRESS, 10), TRANSFER_OK); + assertEq(wrapper.detectTransferRestriction(ATTACKER, ZERO_ADDRESS, 10), CODE_ADDRESS_FROM_NOT_WHITELISTED); + + // Disabling the flags refuses the operation outright. + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + wrapper.setAllowMint(false); + wrapper.setAllowBurn(false); + vm.stopPrank(); + assertEq(wrapper.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, 10), CODE_MINT_NOT_ALLOWED); + assertEq(wrapper.detectTransferRestriction(ADDRESS1, ZERO_ADDRESS, 10), CODE_BURN_NOT_ALLOWED); + } + + function test_Wrapper_SettersAreAdminOnly() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelistWrapper wrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + + vm.prank(ATTACKER); + vm.expectRevert(); + wrapper.setAllowMint(false); + assertTrue(wrapper.allowMint()); + } + + function test_WrapperOwnable2Step_SettersAreOwnerOnly() public { + RuleWhitelistWrapperOwnable2Step wrapper = new RuleWhitelistWrapperOwnable2Step(OWNER, FORWARDER, false, false); + + vm.prank(ATTACKER); + vm.expectRevert(); + wrapper.setAllowBurn(true); + + vm.startPrank(OWNER); + wrapper.setAllowMint(true); + wrapper.setAllowBurn(true); + wrapper.setMaxRules(3); // also covers the wrapper's rules-limit hook + vm.stopPrank(); + + assertTrue(wrapper.allowMint() && wrapper.allowBurn()); + assertEq(wrapper.maxRules(), 3); + } + + function test_Wrapper_SetMaxRulesIsRulesManagerOnly() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelistWrapper wrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + + vm.prank(ATTACKER); + vm.expectRevert(); + wrapper.setMaxRules(3); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + wrapper.setMaxRules(3); + assertEq(wrapper.maxRules(), 3); + } + + /*////////////////////////////////////////////////////////////// + RuleERC2980 (both variants) + //////////////////////////////////////////////////////////////*/ + + function test_ERC2980_SettersToggleBothFlagsIndependently() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleERC2980 e = new RuleERC2980(DEFAULT_ADMIN_ADDRESS, FORWARDER, true); + e.addWhitelistAddress(ADDRESS2); + + assertEq(e.detectTransferRestriction(ZERO_ADDRESS, ADDRESS2, 10), TRANSFER_OK); + assertEq(e.detectTransferRestriction(ADDRESS1, ZERO_ADDRESS, 10), TRANSFER_OK); + + e.setAllowMint(false); + assertFalse(e.allowMint()); + assertTrue(e.allowBurn()); + assertEq(e.detectTransferRestriction(ZERO_ADDRESS, ADDRESS2, 10), CODE_ERC2980_MINT_NOT_ALLOWED); + assertEq(e.detectTransferRestriction(ADDRESS1, ZERO_ADDRESS, 10), TRANSFER_OK); + + e.setAllowBurn(false); + assertEq(e.detectTransferRestriction(ADDRESS1, ZERO_ADDRESS, 10), CODE_ERC2980_BURN_NOT_ALLOWED); + vm.stopPrank(); + + // A permitted mint still screens the recipient. + vm.prank(DEFAULT_ADMIN_ADDRESS); + e.setAllowMint(true); + assertEq(e.detectTransferRestriction(ZERO_ADDRESS, ADDRESS3, 10), 63); // TO_NOT_WHITELISTED + } + + function test_ERC2980_SettersAreAdminOnly() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleERC2980 e = new RuleERC2980(DEFAULT_ADMIN_ADDRESS, FORWARDER, true); + + vm.prank(ATTACKER); + vm.expectRevert(); + e.setAllowMint(false); + + vm.prank(ATTACKER); + vm.expectRevert(); + e.setAllowBurn(false); + + assertTrue(e.allowMint() && e.allowBurn()); + } + + function test_ERC2980Ownable2Step_SettersAreOwnerOnly() public { + RuleERC2980Ownable2Step e = new RuleERC2980Ownable2Step(OWNER, FORWARDER, false); + assertFalse(e.allowMint()); + assertFalse(e.allowBurn()); + + vm.prank(ATTACKER); + vm.expectRevert(); + e.setAllowMint(true); + + vm.startPrank(OWNER); + e.setAllowMint(true); + e.setAllowBurn(true); + vm.stopPrank(); + + assertTrue(e.allowMint() && e.allowBurn()); + assertFalse(e.whitelist(ZERO_ADDRESS)); + } + + /*////////////////////////////////////////////////////////////// + THE SENTINEL CAN NEVER ENTER A LIST + //////////////////////////////////////////////////////////////*/ + + /// @notice Across every address-set rule, the zero address is rejected on a single add and + /// skipped on a batch add — so the getters are clean by construction. + function test_ZeroAddressCanNeverBeListed() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelist w = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + + vm.expectRevert(RuleAddressSet_ZeroAddressNotAllowed.selector); + w.addAddress(ZERO_ADDRESS); + + // A batch containing the sentinel REVERTS: silently skipping it would make the emitted + // `AddAddresses` event report a member that is not in the set. + address[] memory bad = new address[](2); + bad[0] = ZERO_ADDRESS; + bad[1] = ADDRESS1; + vm.expectRevert(RuleAddressSet_ZeroAddressNotAllowed.selector); + w.addAddresses(bad); + + // A clean batch works, and duplicates are still skipped (the event stays truthful). + address[] memory good = new address[](3); + good[0] = ADDRESS1; + good[1] = ADDRESS2; + good[2] = ADDRESS1; // duplicate + w.addAddresses(good); + vm.stopPrank(); + + assertFalse(w.isAddressListed(ZERO_ADDRESS)); + assertTrue(w.isAddressListed(ADDRESS1)); + assertTrue(w.isAddressListed(ADDRESS2)); + assertEq(w.listedAddressCount(), 2); + } +} diff --git a/test/Ownable2Step/Ownable2StepERC165Support.t.sol b/test/Ownable2Step/Ownable2StepERC165Support.t.sol new file mode 100644 index 0000000..68c8607 --- /dev/null +++ b/test/Ownable2Step/Ownable2StepERC165Support.t.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {OwnableInterfaceId} from "RuleEngine/modules/library/OwnableInterfaceId.sol"; +import {Ownable2StepInterfaceId} from "RuleEngine/modules/library/Ownable2StepInterfaceId.sol"; + +import {RuleBlacklistOwnable2Step} from "src/rules/validation/deployment/RuleBlacklistOwnable2Step.sol"; +import {RuleWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleWhitelistOwnable2Step.sol"; +import {RuleWhitelistWrapperOwnable2Step} from "src/rules/validation/deployment/RuleWhitelistWrapperOwnable2Step.sol"; +import {RuleSpenderWhitelistOwnable2Step} from "src/rules/validation/deployment/RuleSpenderWhitelistOwnable2Step.sol"; +import {RuleERC2980Ownable2Step} from "src/rules/validation/deployment/RuleERC2980Ownable2Step.sol"; +import {RuleSanctionsListOwnable2Step} from "src/rules/validation/deployment/RuleSanctionsListOwnable2Step.sol"; +import {RuleIdentityRegistryOwnable2Step} from "src/rules/validation/deployment/RuleIdentityRegistryOwnable2Step.sol"; +import {RuleMaxTotalSupplyOwnable2Step} from "src/rules/validation/deployment/RuleMaxTotalSupplyOwnable2Step.sol"; +import { + RuleConditionalTransferLightOwnable2Step +} from "src/rules/operation/RuleConditionalTransferLightOwnable2Step.sol"; +import { + RuleConditionalTransferLightMultiTokenOwnable2Step +} from "src/rules/operation/RuleConditionalTransferLightMultiTokenOwnable2Step.sol"; +import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol"; + +contract Ownable2StepERC165SupportTest is Test { + address internal constant OWNER = address(0xA11CE); + + function testAllOwnable2StepRulesAdvertiseOwnableInterfaces() public { + RuleBlacklistOwnable2Step blacklist = new RuleBlacklistOwnable2Step(OWNER, address(0)); + RuleWhitelistOwnable2Step whitelist = new RuleWhitelistOwnable2Step(OWNER, address(0), false, false); + RuleWhitelistWrapperOwnable2Step wrapper = new RuleWhitelistWrapperOwnable2Step(OWNER, address(0), false, true); + RuleSpenderWhitelistOwnable2Step spenderWhitelist = new RuleSpenderWhitelistOwnable2Step(OWNER, address(0)); + RuleERC2980Ownable2Step erc2980 = new RuleERC2980Ownable2Step(OWNER, address(0), false); + RuleSanctionsListOwnable2Step sanctions = + new RuleSanctionsListOwnable2Step(OWNER, address(0), ISanctionsList(address(0))); + RuleIdentityRegistryOwnable2Step identity = + new RuleIdentityRegistryOwnable2Step(OWNER, address(0), false, false); + RuleMaxTotalSupplyOwnable2Step maxSupply = new RuleMaxTotalSupplyOwnable2Step(OWNER, address(1), 1); + RuleConditionalTransferLightOwnable2Step conditional = new RuleConditionalTransferLightOwnable2Step(OWNER); + RuleConditionalTransferLightMultiTokenOwnable2Step conditionalMulti = + new RuleConditionalTransferLightMultiTokenOwnable2Step(OWNER); + + _assertOwnable2StepInterfaces(address(blacklist)); + _assertOwnable2StepInterfaces(address(whitelist)); + _assertOwnable2StepInterfaces(address(wrapper)); + _assertOwnable2StepInterfaces(address(spenderWhitelist)); + _assertOwnable2StepInterfaces(address(erc2980)); + _assertOwnable2StepInterfaces(address(sanctions)); + _assertOwnable2StepInterfaces(address(identity)); + _assertOwnable2StepInterfaces(address(maxSupply)); + _assertOwnable2StepInterfaces(address(conditional)); + _assertOwnable2StepInterfaces(address(conditionalMulti)); + } + + function _assertOwnable2StepInterfaces(address target) internal view { + IERC165 i = IERC165(target); + assertTrue(i.supportsInterface(type(IERC165).interfaceId)); + assertTrue(i.supportsInterface(OwnableInterfaceId.IERC173_INTERFACE_ID)); + assertTrue(i.supportsInterface(Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID)); + assertFalse(i.supportsInterface(type(IAccessControl).interfaceId)); + assertFalse(i.supportsInterface(bytes4(0xdeadbeef))); + } +} diff --git a/test/RuleBlacklist/CMTATIntegration.t.sol b/test/RuleBlacklist/CMTATIntegration.t.sol index 8bd6a1a..cb3d033 100644 --- a/test/RuleBlacklist/CMTATIntegration.t.sol +++ b/test/RuleBlacklist/CMTATIntegration.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {HelperContract} from "../HelperContract.sol"; import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; -import {CMTATDeployment} from "RuleEngine/../test/utils/CMTATDeployment.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol"; /** @@ -207,8 +207,9 @@ contract CMTATIntegration is Test, HelperContract { // Act vm.expectRevert( abi.encodeWithSelector( - RuleBlacklist_InvalidTransfer.selector, + RuleBlacklist_InvalidTransferFrom.selector, address(ruleBlacklist), + DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ADDRESS1, amount, diff --git a/test/RuleBlacklist/CMTATIntegrationDirect.t.sol b/test/RuleBlacklist/CMTATIntegrationDirect.t.sol index 9e47b29..9e9f715 100644 --- a/test/RuleBlacklist/CMTATIntegrationDirect.t.sol +++ b/test/RuleBlacklist/CMTATIntegrationDirect.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {HelperContract} from "../HelperContract.sol"; -import {CMTATDeployment} from "RuleEngine/../test/utils/CMTATDeployment.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol"; import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; @@ -178,8 +178,9 @@ contract CMTATIntegrationDirectBlacklist is Test, HelperContract { vm.expectRevert( abi.encodeWithSelector( - RuleBlacklist_InvalidTransfer.selector, + RuleBlacklist_InvalidTransferFrom.selector, address(ruleBlacklist), + DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ADDRESS1, amount, diff --git a/test/RuleConditionalTransferLight/RuleConditionalTransferLightBindRuleEngine.t.sol b/test/RuleConditionalTransferLight/RuleConditionalTransferLightBindRuleEngine.t.sol new file mode 100644 index 0000000..55e2141 --- /dev/null +++ b/test/RuleConditionalTransferLight/RuleConditionalTransferLightBindRuleEngine.t.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; +import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; +import {RuleConditionalTransferLight} from "src/rules/operation/RuleConditionalTransferLight.sol"; + +/** + * @title RuleConditionalTransferLightBindRuleEngine + * @notice Covers the split binding introduced by improvement I-5 (finding F-3, threat `CTL-1`). + * @dev `bindToken` used to serve two conflicting roles at once — the ERC-20 target of + * `approveAndTransferIfAllowed` AND the authorized caller of `transferred`. Behind a RuleEngine + * those are two different addresses, and the single slot could only hold one, so the helper was + * unusable. `bindRuleEngine` splits them: bind the token as the ERC-20 target, bind the engine as + * an additional authorized caller, and the helper works in both topologies. + */ +contract RuleConditionalTransferLightBindRuleEngine is Test, HelperContract { + RuleConditionalTransferLight private rule; + + function setUp() public { + cmtatDeployment = new CMTATDeployment(); + cmtatContract = cmtatDeployment.cmtat(); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, address(cmtatContract)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock.addRule(rule); + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.setRuleEngine(ruleEngineMock); + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.grantRole(keccak256("MINTER_ROLE"), DEFAULT_ADMIN_ADDRESS); + } + + /// @dev The supported RuleEngine wiring: token bound as the ERC-20, engine bound as a caller. + function _bindBoth() private { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(address(cmtatContract)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindRuleEngine(address(ruleEngineMock)); + } + + /*////////////////////////////////////////////////////////////// + THE FIX: BOTH TOPOLOGIES WORK + //////////////////////////////////////////////////////////////*/ + + /** + * @notice The whole point of I-5: with the token AND the engine bound, + * `approveAndTransferIfAllowed` now works end-to-end behind a RuleEngine. + */ + function test_ApproveAndTransferIfAllowedWorksBehindRuleEngine() public { + _bindBoth(); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.mint(ADDRESS1, 100); + vm.prank(ADDRESS1); + cmtatContract.approve(address(rule), 1000); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + assertTrue(rule.approveAndTransferIfAllowed(ADDRESS1, ADDRESS2, 10)); + + assertEq(cmtatContract.balanceOf(ADDRESS2), 10); + assertEq(cmtatContract.balanceOf(ADDRESS1), 90); + // The approval recorded by the helper was consumed by the engine's callback. + assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, 10), 0); + } + + /** + * @notice Ordinary transfers also work: the engine is now an authorized executor, so the + * approval it consumes is the one the operator recorded. + */ + function test_OperatorApprovedTransferIsConsumedByTheEngine() public { + _bindBoth(); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.mint(ADDRESS1, 100); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(ADDRESS1, ADDRESS2, 25); + assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, 25), 1); + + vm.prank(ADDRESS1); + cmtatContract.transfer(ADDRESS2, 25); + + assertEq(cmtatContract.balanceOf(ADDRESS2), 25); + assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, 25), 0); + } + + /** + * @notice An unapproved transfer is still rejected through the engine. + */ + function test_UnapprovedTransferStillRejectedBehindRuleEngine() public { + _bindBoth(); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.mint(ADDRESS1, 100); + + vm.prank(ADDRESS1); + vm.expectRevert(TransferNotApproved.selector); + cmtatContract.transfer(ADDRESS2, 25); + } + + /** + * @notice Direct binding still works unchanged: a token bound alone is its own executor. + */ + function test_DirectBindingStillWorks() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); + + assertTrue(rule.isTransferExecutor(ADDRESS1)); + assertEq(rule.ruleEngine(), ZERO_ADDRESS); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(ADDRESS2, ADDRESS3, 10); + vm.prank(ADDRESS1); + rule.transferred(ADDRESS2, ADDRESS3, 10); + assertEq(rule.approvedCount(ADDRESS2, ADDRESS3, 10), 0); + } + + /*////////////////////////////////////////////////////////////// + AUTHORIZATION + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Without `bindRuleEngine`, the engine remains unauthorized — the old failure mode. + * This is what makes the new binding necessary rather than cosmetic. + */ + function test_EngineUnauthorizedUntilBound() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(address(cmtatContract)); + + assertFalse(rule.isTransferExecutor(address(ruleEngineMock))); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectRevert( + abi.encodeWithSelector( + RuleConditionalTransferLight_TransferExecutorUnauthorized.selector, address(ruleEngineMock) + ) + ); + cmtatContract.mint(ADDRESS1, 100); + + // Binding the engine unblocks it. + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindRuleEngine(address(ruleEngineMock)); + assertTrue(rule.isTransferExecutor(address(ruleEngineMock))); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.mint(ADDRESS1, 100); + assertEq(cmtatContract.balanceOf(ADDRESS1), 100); + } + + /** + * @notice An arbitrary address is never a transfer executor. + */ + function test_ArbitraryCallerIsNotExecutor() public { + _bindBoth(); + + assertFalse(rule.isTransferExecutor(ATTACKER)); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(ADDRESS1, ADDRESS2, 10); + + vm.prank(ATTACKER); + vm.expectRevert( + abi.encodeWithSelector(RuleConditionalTransferLight_TransferExecutorUnauthorized.selector, ATTACKER) + ); + rule.transferred(ADDRESS1, ADDRESS2, 10); + + assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, 10), 1); + } + + /*////////////////////////////////////////////////////////////// + BINDING LIFECYCLE + //////////////////////////////////////////////////////////////*/ + + function test_BindRuleEngineGuards() public { + // zero address rejected + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectRevert(RuleConditionalTransferLight_RuleEngineAddressZeroNotAllowed.selector); + rule.bindRuleEngine(ZERO_ADDRESS); + + // second binding rejected until unbound + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindRuleEngine(address(ruleEngineMock)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectRevert(RuleConditionalTransferLight_RuleEngineAlreadyBound.selector); + rule.bindRuleEngine(ATTACKER); + + // unbind, then rebind + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.unbindRuleEngine(); + assertEq(rule.ruleEngine(), ZERO_ADDRESS); + assertFalse(rule.isTransferExecutor(address(ruleEngineMock))); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindRuleEngine(ATTACKER); + assertEq(rule.ruleEngine(), ATTACKER); + } + + function test_UnbindRuleEngineRevertsWhenNoneBound() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectRevert(RuleConditionalTransferLight_RuleEngineNotBound.selector); + rule.unbindRuleEngine(); + } + + function test_BindRuleEngineIsComplianceManagerOnly() public { + vm.prank(ATTACKER); + vm.expectRevert(); + rule.bindRuleEngine(address(ruleEngineMock)); + + _bindBoth(); + + vm.prank(ATTACKER); + vm.expectRevert(); + rule.unbindRuleEngine(); + + assertEq(rule.ruleEngine(), address(ruleEngineMock)); + } +} diff --git a/test/RuleConditionalTransferLightMultiToken/CMTATIntegrationDirectMultiToken.t.sol b/test/RuleConditionalTransferLightMultiToken/CMTATIntegrationDirectMultiToken.t.sol new file mode 100644 index 0000000..7c67eb3 --- /dev/null +++ b/test/RuleConditionalTransferLightMultiToken/CMTATIntegrationDirectMultiToken.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; +import {CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol"; + +contract CMTATIntegrationDirectMultiToken is Test, HelperContract { + CMTATStandardStandalone private tokenA; + CMTATStandardStandalone private tokenB; + RuleConditionalTransferLightMultiToken private rule; + + function setUp() public { + CMTATDeployment deploymentA = new CMTATDeployment(); + CMTATDeployment deploymentB = new CMTATDeployment(); + + tokenA = deploymentA.cmtat(); + tokenB = deploymentB.cmtat(); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS); + + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(address(tokenA)); + rule.bindToken(address(tokenB)); + tokenA.setRuleEngine(IRuleEngine(address(rule))); + tokenB.setRuleEngine(IRuleEngine(address(rule))); + + tokenA.mint(ADDRESS1, 100); + tokenB.mint(ADDRESS1, 100); + vm.stopPrank(); + } + + function testDetectRestriction_IsTokenScoped() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(address(tokenA), ADDRESS1, ADDRESS2, 10); + + resUint8 = tokenA.detectTransferRestriction(ADDRESS1, ADDRESS2, 10); + assertEq(resUint8, TRANSFER_OK); + + resUint8 = tokenB.detectTransferRestriction(ADDRESS1, ADDRESS2, 10); + assertEq(resUint8, CODE_TRANSFER_REQUEST_NOT_APPROVED); + } + + function testTransferConsumption_IsTokenScoped() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(address(tokenA), ADDRESS1, ADDRESS2, 10); + + vm.prank(ADDRESS1); + tokenA.transfer(ADDRESS2, 10); + + assertEq(rule.approvedCount(address(tokenA), ADDRESS1, ADDRESS2, 10), 0); + assertEq(rule.approvedCount(address(tokenB), ADDRESS1, ADDRESS2, 10), 0); + + vm.prank(ADDRESS1); + vm.expectRevert(); + tokenB.transfer(ADDRESS2, 10); + } +} diff --git a/test/RuleConditionalTransferLightMultiToken/MultiTokenPreflightView.t.sol b/test/RuleConditionalTransferLightMultiToken/MultiTokenPreflightView.t.sol new file mode 100644 index 0000000..9082103 --- /dev/null +++ b/test/RuleConditionalTransferLightMultiToken/MultiTokenPreflightView.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol"; + +/** + * @title MultiTokenPreflightView + * @notice Covers the caller-explicit pre-flight views added by improvement I-7 (threat `CTL-4`, + * finding F-8). + * @dev `detectTransferRestriction` / `canTransfer` derive the token key from `msg.sender`, so any + * caller that is not the bound token reads "not approved" even for an approved transfer. + * `detectTransferRestrictionForToken` / `canTransferForToken` take the token explicitly and + * therefore give every caller the real answer. + */ +contract MultiTokenPreflightView is Test, HelperContract { + RuleConditionalTransferLightMultiToken private rule; + + function setUp() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); + rule.approveTransfer(ADDRESS1, ADDRESS2, ADDRESS3, 10); + vm.stopPrank(); + } + + /** + * @notice The explicit view returns the true answer regardless of who asks — including an + * arbitrary off-chain caller, which is exactly the case the ERC-1404 view cannot serve. + */ + function test_ExplicitViewIsCallerIndependent() public { + // The bound token gets the right answer from both views. + vm.prank(ADDRESS1); + assertEq(rule.detectTransferRestriction(ADDRESS2, ADDRESS3, 10), TRANSFER_OK); + assertEq(rule.detectTransferRestrictionForToken(ADDRESS1, ADDRESS2, ADDRESS3, 10), TRANSFER_OK); + + // An unrelated caller gets a MISLEADING answer from the ERC-1404 view... + vm.prank(ATTACKER); + assertEq(rule.detectTransferRestriction(ADDRESS2, ADDRESS3, 10), CODE_TRANSFER_REQUEST_NOT_APPROVED); + + // ...but the correct one from the explicit view. + vm.prank(ATTACKER); + assertEq(rule.detectTransferRestrictionForToken(ADDRESS1, ADDRESS2, ADDRESS3, 10), TRANSFER_OK); + } + + /** + * @notice `canTransferForToken` is the boolean counterpart and is likewise caller-independent. + */ + function test_CanTransferForTokenIsCallerIndependent() public { + vm.prank(ATTACKER); + assertEq(rule.canTransfer(ADDRESS2, ADDRESS3, 10), false, "caller-dependent view misleads"); + + vm.prank(ATTACKER); + assertEq(rule.canTransferForToken(ADDRESS1, ADDRESS2, ADDRESS3, 10), true, "explicit view is truthful"); + } + + /** + * @notice The explicit view reports an unapproved transfer as restricted. + */ + function test_ExplicitViewReportsUnapprovedTransfer() public view { + assertEq( + rule.detectTransferRestrictionForToken(ADDRESS1, ADDRESS2, ADDRESS3, 999), + CODE_TRANSFER_REQUEST_NOT_APPROVED + ); + assertEq(rule.canTransferForToken(ADDRESS1, ADDRESS2, ADDRESS3, 999), false); + } + + /** + * @notice Approvals stay token-scoped in the explicit view: an unbound token is fail-closed. + */ + function test_ExplicitViewIsTokenScopedAndFailsClosedForUnboundToken() public view { + // ADDRESS2 was never bound, so it has no consumable approvals. + assertEq( + rule.detectTransferRestrictionForToken(ADDRESS2, ADDRESS2, ADDRESS3, 10), CODE_TRANSFER_REQUEST_NOT_APPROVED + ); + assertEq(rule.canTransferForToken(ADDRESS2, ADDRESS2, ADDRESS3, 10), false); + } + + /** + * @notice Mint and burn are exempt on the explicit view too, matching the enforcement path. + */ + function test_ExplicitViewExemptsMintAndBurn() public view { + assertEq(rule.detectTransferRestrictionForToken(ADDRESS1, ZERO_ADDRESS, ADDRESS3, 10), TRANSFER_OK); + assertEq(rule.detectTransferRestrictionForToken(ADDRESS1, ADDRESS2, ZERO_ADDRESS, 10), TRANSFER_OK); + } + + /** + * @notice The explicit view tracks consumption: once the approval is spent it reports restricted. + */ + function test_ExplicitViewTracksConsumption() public { + assertEq(rule.canTransferForToken(ADDRESS1, ADDRESS2, ADDRESS3, 10), true); + + vm.prank(ADDRESS1); + rule.transferred(ADDRESS2, ADDRESS3, 10); + + assertEq(rule.canTransferForToken(ADDRESS1, ADDRESS2, ADDRESS3, 10), false); + assertEq( + rule.detectTransferRestrictionForToken(ADDRESS1, ADDRESS2, ADDRESS3, 10), CODE_TRANSFER_REQUEST_NOT_APPROVED + ); + } + + /** + * @notice The two views can never disagree: both are backed by the same internal helper, so for + * the bound token the ERC-1404 view and the explicit view always agree. + */ + function testFuzz_ViewsAgreeForTheBoundToken(address from, address to, uint256 value) public { + vm.assume(from != ZERO_ADDRESS && to != ZERO_ADDRESS); + + vm.prank(ADDRESS1); + uint8 implicitCode = rule.detectTransferRestriction(from, to, value); + uint8 explicitCode = rule.detectTransferRestrictionForToken(ADDRESS1, from, to, value); + + assertEq(implicitCode, explicitCode, "implicit and explicit views disagree"); + } +} diff --git a/test/RuleConditionalTransferLightMultiToken/MultiTokenSurface.t.sol b/test/RuleConditionalTransferLightMultiToken/MultiTokenSurface.t.sol new file mode 100644 index 0000000..e88690a --- /dev/null +++ b/test/RuleConditionalTransferLightMultiToken/MultiTokenSurface.t.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol"; + +/** + * @title MultiTokenSurface + * @notice Closes the residual coverage gap on `RuleConditionalTransferLightMultiToken`: the + * `created` / `destroyed` compliance hooks, `cancelTransferApproval`, the restriction-code + * metadata, and the spender-aware read views. + */ +contract MultiTokenSurface is Test, HelperContract { + /// @dev Redeclared locally: `HelperContract` inherits the single-token invariant storage, whose + /// constants clash with the multi-token variant's. + error RuleConditionalTransferLightMultiToken_TransferApprovalNotFound(); + + uint8 private constant CODE_NOT_APPROVED = 46; + + RuleConditionalTransferLightMultiToken private rule; + + function setUp() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////// + RESTRICTION-CODE METADATA + //////////////////////////////////////////////////////////////*/ + + function test_CanReturnTransferRestrictionCode() public view { + assertTrue(rule.canReturnTransferRestrictionCode(CODE_NOT_APPROVED)); + assertFalse(rule.canReturnTransferRestrictionCode(CODE_NONEXISTENT)); + } + + function test_MessageForTransferRestriction() public view { + assertEq( + rule.messageForTransferRestriction(CODE_NOT_APPROVED), + "ConditionalTransferLightMultiToken: The request is not approved" + ); + assertEq(rule.messageForTransferRestriction(CODE_NONEXISTENT), TEXT_CODE_NOT_FOUND); + } + + /*////////////////////////////////////////////////////////////// + MINT / BURN COMPLIANCE HOOKS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice `created` / `destroyed` are exempt from approval consumption (mint/burn), so they + * succeed without any approval and leave `approvalCounts` untouched. + */ + function test_CreatedAndDestroyedAreExemptFromApprovalConsumption() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(ADDRESS1, ADDRESS2, ADDRESS3, 10); + assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, ADDRESS3, 10), 1); + + vm.startPrank(ADDRESS1); // the bound token + rule.created(ADDRESS2, 10); + rule.destroyed(ADDRESS2, 10); + vm.stopPrank(); + + // The approval was NOT consumed by the mint/burn hooks. + assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, ADDRESS3, 10), 1); + } + + /// @notice Only the bound token may call the mint/burn hooks. + function test_CreatedAndDestroyedRejectUnboundCaller() public { + vm.prank(ATTACKER); + vm.expectRevert(); + rule.created(ADDRESS2, 10); + + vm.prank(ATTACKER); + vm.expectRevert(); + rule.destroyed(ADDRESS2, 10); + } + + /*////////////////////////////////////////////////////////////// + CANCEL APPROVAL + //////////////////////////////////////////////////////////////*/ + + function test_CancelTransferApprovalDecrementsByOne() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(ADDRESS1, ADDRESS2, ADDRESS3, 10); + rule.approveTransfer(ADDRESS1, ADDRESS2, ADDRESS3, 10); + assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, ADDRESS3, 10), 2); + + rule.cancelTransferApproval(ADDRESS1, ADDRESS2, ADDRESS3, 10); + vm.stopPrank(); + + // Exactly one removed (contrast with `resetApproval`, which clears them all). + assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, ADDRESS3, 10), 1); + } + + function test_CancelTransferApprovalRevertsWhenNoneExists() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectRevert(RuleConditionalTransferLightMultiToken_TransferApprovalNotFound.selector); + rule.cancelTransferApproval(ADDRESS1, ADDRESS2, ADDRESS3, 10); + } + + function test_CancelTransferApprovalIsApproverOnly() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(ADDRESS1, ADDRESS2, ADDRESS3, 10); + + vm.prank(ATTACKER); + vm.expectRevert(); + rule.cancelTransferApproval(ADDRESS1, ADDRESS2, ADDRESS3, 10); + + assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, ADDRESS3, 10), 1); + } + + /*////////////////////////////////////////////////////////////// + SPENDER-AWARE READ VIEWS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice The spender-aware ERC-1404 / ERC-7551 views ignore the spender and delegate to the + * caller-keyed `detectTransferRestriction`, so they carry the same caller dependence. + */ + function test_SpenderAwareViewsDelegateToTheCallerKeyedView() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(ADDRESS1, ADDRESS2, ADDRESS3, 10); + + // Called BY the bound token: approved. + vm.prank(ADDRESS1); + assertEq(rule.detectTransferRestrictionFrom(ATTACKER, ADDRESS2, ADDRESS3, 10), TRANSFER_OK); + vm.prank(ADDRESS1); + assertTrue(rule.canTransferFrom(ATTACKER, ADDRESS2, ADDRESS3, 10)); + + // Called by anyone else: fail-closed (use the `…ForToken` views instead). + vm.prank(ATTACKER); + assertEq(rule.detectTransferRestrictionFrom(ATTACKER, ADDRESS2, ADDRESS3, 10), CODE_NOT_APPROVED); + vm.prank(ATTACKER); + assertFalse(rule.canTransferFrom(ATTACKER, ADDRESS2, ADDRESS3, 10)); + } +} diff --git a/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiToken.t.sol b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiToken.t.sol new file mode 100644 index 0000000..9cbd950 --- /dev/null +++ b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiToken.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol"; +import {MockERC20WithTransferContext} from "src/mocks/MockERC20WithTransferContext.sol"; + +contract RuleConditionalTransferLightMultiTokenTest is Test, HelperContract { + RuleConditionalTransferLightMultiToken private rule; + MockERC20WithTransferContext private tokenA; + MockERC20WithTransferContext private tokenB; + + function setUp() public { + tokenA = new MockERC20WithTransferContext("Token A", "TKNA"); + tokenB = new MockERC20WithTransferContext("Token B", "TKNB"); + + rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS); + + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(address(tokenA)); + rule.bindToken(address(tokenB)); + vm.stopPrank(); + + tokenA.setRule(address(rule)); + tokenB.setRule(address(rule)); + + tokenA.mint(ADDRESS1, 100); + tokenB.mint(ADDRESS1, 100); + } + + function testApprovalForTokenADoesNotAuthorizeTokenB() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(address(tokenA), ADDRESS1, ADDRESS2, 10); + + vm.prank(ADDRESS1); + tokenA.transfer(ADDRESS2, 10); + + assertEq(tokenA.balanceOf(ADDRESS1), 90); + assertEq(tokenA.balanceOf(ADDRESS2), 10); + + vm.expectRevert(); + vm.prank(ADDRESS1); + tokenB.transfer(ADDRESS2, 10); + } + + function testApproveAndTransferIfAllowedUsesTokenScopedApproval() public { + vm.prank(ADDRESS1); + tokenA.approve(address(rule), 10); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveAndTransferIfAllowed(address(tokenA), ADDRESS1, ADDRESS2, 10); + + assertEq(tokenA.balanceOf(ADDRESS1), 90); + assertEq(tokenA.balanceOf(ADDRESS2), 10); + assertEq(rule.approvedCount(address(tokenA), ADDRESS1, ADDRESS2, 10), 0); + assertEq(rule.approvedCount(address(tokenB), ADDRESS1, ADDRESS2, 10), 0); + } + + function testApproveTransferRevertsForUnboundToken() public { + vm.expectRevert(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(ADDRESS3, ADDRESS1, ADDRESS2, 10); + } +} diff --git a/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiTokenRuleEngineIntegration.t.sol b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiTokenRuleEngineIntegration.t.sol new file mode 100644 index 0000000..e652b66 --- /dev/null +++ b/test/RuleConditionalTransferLightMultiToken/RuleConditionalTransferLightMultiTokenRuleEngineIntegration.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; +import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol"; + +contract RuleConditionalTransferLightMultiTokenRuleEngineIntegrationTest is Test, HelperContract { + RuleConditionalTransferLightMultiToken private rule; + RuleEngine private sharedRuleEngine; + + function setUp() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + sharedRuleEngine = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS); + + // In RuleEngine path, rule sees only msg.sender == RuleEngine. + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(address(sharedRuleEngine)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS2); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + sharedRuleEngine.addRule(rule); + } + + function testTokenScopedApprovalIsNotVisibleThroughSharedRuleEngineCallerContext() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(ADDRESS1, ADDRESS2, ADDRESS3, 10); + + resUint8 = sharedRuleEngine.detectTransferRestriction(ADDRESS2, ADDRESS3, 10); + assertEq(resUint8, CODE_TRANSFER_REQUEST_NOT_APPROVED); + } + + function testRuleEngineScopedApprovalIsConsumableButNotTokenScoped() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(address(sharedRuleEngine), ADDRESS2, ADDRESS3, 10); + + resUint8 = sharedRuleEngine.detectTransferRestriction(ADDRESS2, ADDRESS3, 10); + assertEq(resUint8, TRANSFER_OK); + + vm.prank(address(sharedRuleEngine)); + rule.transferred(ADDRESS2, ADDRESS3, 10); + + resUint8 = sharedRuleEngine.detectTransferRestriction(ADDRESS2, ADDRESS3, 10); + assertEq(resUint8, CODE_TRANSFER_REQUEST_NOT_APPROVED); + } +} diff --git a/test/RuleERC2980/Ownable/RuleERC2980OwnableAccessControl.t.sol b/test/RuleERC2980/Ownable/RuleERC2980OwnableAccessControl.t.sol index 0660fe6..888fdbc 100644 --- a/test/RuleERC2980/Ownable/RuleERC2980OwnableAccessControl.t.sol +++ b/test/RuleERC2980/Ownable/RuleERC2980OwnableAccessControl.t.sol @@ -133,8 +133,12 @@ contract RuleERC2980OwnableAccessControl is Test, HelperContract { rule.addFrozenlistAddresses(addrs); } - function testAllowBurnConstructorWhitelistsZeroAddress() public { + /// @notice `allowMintBurn` permits burning via the explicit flag, without listing the sentinel. + function testAllowMintBurnConstructorSetsFlagsAndDoesNotListZeroAddress() public { RuleERC2980Ownable2Step burnEnabled = new RuleERC2980Ownable2Step(OWNER, ZERO_ADDRESS, true); - assertTrue(burnEnabled.isWhitelisted(ZERO_ADDRESS)); + assertTrue(burnEnabled.allowMint()); + assertTrue(burnEnabled.allowBurn()); + assertFalse(burnEnabled.isWhitelisted(ZERO_ADDRESS)); + assertFalse(burnEnabled.whitelist(ZERO_ADDRESS)); } } diff --git a/test/RuleERC2980/RuleERC2980.t.sol b/test/RuleERC2980/RuleERC2980.t.sol index 7009235..317a79c 100644 --- a/test/RuleERC2980/RuleERC2980.t.sol +++ b/test/RuleERC2980/RuleERC2980.t.sol @@ -16,6 +16,8 @@ contract RuleERC2980Test is Test, HelperContract { uint8 constant CODE_TO_FROZEN = 61; uint8 constant CODE_SPENDER_FROZEN = 62; uint8 constant CODE_TO_NOT_WHITELISTED = 63; + uint8 constant CODE_ERC2980_MINT_NOT_ALLOWED = 64; + uint8 constant CODE_ERC2980_BURN_NOT_ALLOWED = 65; function setUp() public { vm.prank(DEFAULT_ADMIN_ADDRESS); @@ -104,21 +106,48 @@ contract RuleERC2980Test is Test, HelperContract { assertEq(resUint8, NO_ERROR); } - function testBurnRecipientNotWhitelistedByDefault() public { + /// @notice With `allowMintBurn = false`, a burn is refused with the DEDICATED code, not with + /// "recipient not whitelisted" — the zero address is a sentinel, not a recipient. + function testBurnRefusedWithDedicatedCodeByDefault() public { + assertFalse(ruleERC2980.allowBurn()); assertFalse(ruleERC2980.whitelist(ZERO_ADDRESS)); resUint8 = ruleERC2980.detectTransferRestriction(ADDRESS1, ZERO_ADDRESS, 20); - assertEq(resUint8, CODE_TO_NOT_WHITELISTED); + assertEq(resUint8, CODE_ERC2980_BURN_NOT_ALLOWED); } - function testAllowBurnConstructorWhitelistsZeroAddress() public { + /// @notice `allowMintBurn` permits burning WITHOUT whitelisting the zero address, so the + /// MANDATORY ERC-2980 getter `whitelist(address(0))` stays truthful (`false`). + function testAllowMintBurnConstructorPermitsBurnWithoutWhitelistingZeroAddress() public { vm.prank(DEFAULT_ADMIN_ADDRESS); RuleERC2980 burnEnabledRule = new RuleERC2980(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true); - assertTrue(burnEnabledRule.whitelist(ZERO_ADDRESS)); + assertTrue(burnEnabledRule.allowMint()); + assertTrue(burnEnabledRule.allowBurn()); + + // The ERC-2980 mandatory getter no longer lies about the sentinel. + assertFalse(burnEnabledRule.whitelist(ZERO_ADDRESS)); + assertFalse(burnEnabledRule.isWhitelisted(ZERO_ADDRESS)); + assertFalse(burnEnabledRule.frozenlist(ZERO_ADDRESS)); + assertFalse(burnEnabledRule.isVerified(ZERO_ADDRESS)); + resUint8 = burnEnabledRule.detectTransferRestriction(ADDRESS1, ZERO_ADDRESS, 20); assertEq(resUint8, NO_ERROR); } + /// @notice The zero address can never enter either ERC-2980 list. + function testCannotListZeroAddress() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + vm.expectRevert(RuleERC2980InvariantStorage.RuleERC2980_ZeroAddressNotAllowed.selector); + ruleERC2980.addWhitelistAddress(ZERO_ADDRESS); + + vm.expectRevert(RuleERC2980InvariantStorage.RuleERC2980_ZeroAddressNotAllowed.selector); + ruleERC2980.addFrozenlistAddress(ZERO_ADDRESS); + vm.stopPrank(); + + assertFalse(ruleERC2980.whitelist(ZERO_ADDRESS)); + assertFalse(ruleERC2980.frozenlist(ZERO_ADDRESS)); + } + /*////////////////////////////////////////////////////////////// FROZENLIST — SENDER FROZEN //////////////////////////////////////////////////////////////*/ @@ -285,7 +314,7 @@ contract RuleERC2980Test is Test, HelperContract { function testAddWhitelistAddressAlreadyListedReverts() public { vm.prank(DEFAULT_ADMIN_ADDRESS); - vm.expectRevert(RuleERC2980InvariantStorage.RuleERC2980_AddressAlreadyListed.selector); + vm.expectRevert(RuleERC2980InvariantStorage.RuleERC2980_AddressAlreadyWhitelisted.selector); ruleERC2980.addWhitelistAddress(ADDRESS2); } @@ -298,7 +327,7 @@ contract RuleERC2980Test is Test, HelperContract { function testRemoveWhitelistAddressNotFoundReverts() public { vm.prank(DEFAULT_ADMIN_ADDRESS); - vm.expectRevert(RuleERC2980InvariantStorage.RuleERC2980_AddressNotFound.selector); + vm.expectRevert(RuleERC2980InvariantStorage.RuleERC2980_AddressNotWhitelisted.selector); ruleERC2980.removeWhitelistAddress(ADDRESS1); } @@ -350,7 +379,7 @@ contract RuleERC2980Test is Test, HelperContract { vm.prank(DEFAULT_ADMIN_ADDRESS); ruleERC2980.addFrozenlistAddress(ADDRESS1); vm.prank(DEFAULT_ADMIN_ADDRESS); - vm.expectRevert(RuleERC2980InvariantStorage.RuleERC2980_AddressAlreadyListed.selector); + vm.expectRevert(RuleERC2980InvariantStorage.RuleERC2980_AddressAlreadyFrozen.selector); ruleERC2980.addFrozenlistAddress(ADDRESS1); } @@ -365,7 +394,7 @@ contract RuleERC2980Test is Test, HelperContract { function testRemoveFrozenlistAddressNotFoundReverts() public { vm.prank(DEFAULT_ADMIN_ADDRESS); - vm.expectRevert(RuleERC2980InvariantStorage.RuleERC2980_AddressNotFound.selector); + vm.expectRevert(RuleERC2980InvariantStorage.RuleERC2980_AddressNotFrozen.selector); ruleERC2980.removeFrozenlistAddress(ADDRESS1); } diff --git a/test/RuleIdentityRegistry/Ownable/RuleIdentityRegistryOwnable2Step.t.sol b/test/RuleIdentityRegistry/Ownable/RuleIdentityRegistryOwnable2Step.t.sol index 8c347ce..ecd0bce 100644 --- a/test/RuleIdentityRegistry/Ownable/RuleIdentityRegistryOwnable2Step.t.sol +++ b/test/RuleIdentityRegistry/Ownable/RuleIdentityRegistryOwnable2Step.t.sol @@ -11,7 +11,8 @@ contract RuleIdentityRegistryOwnable2StepTest is Ownable2StepTestBase { function _deployOwnable2Step() internal override returns (IOwnable2StepLike, address) { address ownerAddr = WHITELIST_OPERATOR_ADDRESS; IdentityRegistryMock registry = new IdentityRegistryMock(); - RuleIdentityRegistryOwnable2Step rule = new RuleIdentityRegistryOwnable2Step(ownerAddr, address(registry)); + RuleIdentityRegistryOwnable2Step rule = + new RuleIdentityRegistryOwnable2Step(ownerAddr, address(registry), false, false); return (IOwnable2StepLike(address(rule)), ownerAddr); } } @@ -24,7 +25,7 @@ contract RuleIdentityRegistryOwnable2StepAccessControl is Test, HelperContract { function setUp() public { registry = new IdentityRegistryMock(); - rule = new RuleIdentityRegistryOwnable2Step(WHITELIST_OPERATOR_ADDRESS, address(registry)); + rule = new RuleIdentityRegistryOwnable2Step(WHITELIST_OPERATOR_ADDRESS, address(registry), false, false); } function testOwnerCanSetAndClearIdentityRegistry() public { diff --git a/test/RuleIdentityRegistry/RuleIdentityRegistryAccessControlRoleMembers.t.sol b/test/RuleIdentityRegistry/RuleIdentityRegistryAccessControlRoleMembers.t.sol index 1792bd1..5ab7ae3 100644 --- a/test/RuleIdentityRegistry/RuleIdentityRegistryAccessControlRoleMembers.t.sol +++ b/test/RuleIdentityRegistry/RuleIdentityRegistryAccessControlRoleMembers.t.sol @@ -10,7 +10,7 @@ import {RuleIdentityRegistry} from "src/rules/validation/deployment/RuleIdentity contract RuleIdentityRegistryAccessControlRoleMembers is AccessControlEnumerableTestBase { function _deployAccessControl() internal override returns (IAccessControlEnumerableLike, address) { address adminAddr = DEFAULT_ADMIN_ADDRESS; - RuleIdentityRegistry rule = new RuleIdentityRegistry(adminAddr, ADDRESS1); + RuleIdentityRegistry rule = new RuleIdentityRegistry(adminAddr, ADDRESS1, false, false); return (IAccessControlEnumerableLike(address(rule)), adminAddr); } diff --git a/test/RuleIdentityRegistry/RuleIdentityRegistryRuleEngineIntegration.t.sol b/test/RuleIdentityRegistry/RuleIdentityRegistryRuleEngineIntegration.t.sol index 74488cf..08f3bc3 100644 --- a/test/RuleIdentityRegistry/RuleIdentityRegistryRuleEngineIntegration.t.sol +++ b/test/RuleIdentityRegistry/RuleIdentityRegistryRuleEngineIntegration.t.sol @@ -20,20 +20,33 @@ contract RuleIdentityRegistryRuleEngineIntegration is Test, HelperContract { ruleEngineMock = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); vm.prank(DEFAULT_ADMIN_ADDRESS); - ruleIdentityRegistry = new RuleIdentityRegistry(DEFAULT_ADMIN_ADDRESS, address(registry)); + ruleIdentityRegistry = new RuleIdentityRegistry(DEFAULT_ADMIN_ADDRESS, address(registry), false, false); vm.prank(DEFAULT_ADMIN_ADDRESS); ruleEngineMock.addRule(ruleIdentityRegistry); } - function testDetectRestrictionWhenNotVerified() public { + /// @notice ERC-3643 mandates verifying only the RECEIVER, so an unverified recipient is what + /// blocks the transfer — not the sender. + function testDetectRestrictionWhenReceiverNotVerified() public { uint256 amount = 10; resUint8 = ruleEngineMock.detectTransferRestriction(ADDRESS1, ADDRESS2, amount); - assertEq(resUint8, CODE_ADDRESS_FROM_NOT_VERIFIED); + assertEq(resUint8, CODE_ADDRESS_TO_NOT_VERIFIED); resBool = ruleEngineMock.canTransfer(ADDRESS1, ADDRESS2, amount); assertEq(resBool, false); } + /// @notice A verified receiver is sufficient: the sender need NOT be verified (ERC-3643). + /// This is what lets a de-listed holder exit their position. + function testUnverifiedSenderCanStillExitToAVerifiedReceiver() public { + uint256 amount = 10; + registry.setVerified(ADDRESS2, true); // receiver only + + resUint8 = ruleEngineMock.detectTransferRestriction(ADDRESS1, ADDRESS2, amount); + assertEq(resUint8, TRANSFER_OK); + assertTrue(ruleEngineMock.canTransfer(ADDRESS1, ADDRESS2, amount)); + } + function testDetectRestrictionWhenVerified() public { uint256 amount = 10; registry.setVerified(ADDRESS1, true); @@ -45,21 +58,33 @@ contract RuleIdentityRegistryRuleEngineIntegration is Test, HelperContract { assertEq(resBool, true); } - function testDetectRestrictionFromWithSpender() public { + /// @notice ERC-3643: "`transferFrom` works the same way" — the SPENDER need not be verified. + function testDetectRestrictionFromWithUnverifiedSpenderIsAllowed() public { + uint256 amount = 10; + registry.setVerified(ADDRESS1, true); + registry.setVerified(ADDRESS2, true); + // ADDRESS3 (spender) is NOT verified. + + resUint8 = ruleEngineMock.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ADDRESS2, amount); + assertEq(resUint8, TRANSFER_OK); + assertTrue(ruleEngineMock.canTransferFrom(ADDRESS3, ADDRESS1, ADDRESS2, amount)); + } + + /// @notice The stricter spender check remains available as an explicit opt-in. + function testSpenderCheckCanBeOptedIn() public { uint256 amount = 10; registry.setVerified(ADDRESS1, true); registry.setVerified(ADDRESS2, true); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleIdentityRegistry.setCheckSpender(true); + resUint8 = ruleEngineMock.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ADDRESS2, amount); assertEq(resUint8, CODE_ADDRESS_SPENDER_NOT_VERIFIED); - resBool = ruleEngineMock.canTransferFrom(ADDRESS3, ADDRESS1, ADDRESS2, amount); - assertEq(resBool, false); registry.setVerified(ADDRESS3, true); resUint8 = ruleEngineMock.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ADDRESS2, amount); assertEq(resUint8, TRANSFER_OK); - resBool = ruleEngineMock.canTransferFrom(ADDRESS3, ADDRESS1, ADDRESS2, amount); - assertEq(resBool, true); } function testClearIdentityRegistryDisablesChecks() public { diff --git a/test/RuleIdentityRegistry/RuleIdentityRegistryUnit.t.sol b/test/RuleIdentityRegistry/RuleIdentityRegistryUnit.t.sol index 81789ae..1f7f313 100644 --- a/test/RuleIdentityRegistry/RuleIdentityRegistryUnit.t.sol +++ b/test/RuleIdentityRegistry/RuleIdentityRegistryUnit.t.sol @@ -13,19 +13,35 @@ contract RuleIdentityRegistryUnit is Test, HelperContract { function setUp() public { registry = new IdentityRegistryMock(); vm.prank(DEFAULT_ADMIN_ADDRESS); - rule = new RuleIdentityRegistry(DEFAULT_ADMIN_ADDRESS, address(registry)); + rule = new RuleIdentityRegistry(DEFAULT_ADMIN_ADDRESS, address(registry), false, false); } function testDetectRestriction_NoRegistry_ReturnsOk() public { vm.prank(DEFAULT_ADMIN_ADDRESS); - rule = new RuleIdentityRegistry(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS); + rule = new RuleIdentityRegistry(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, false, false); resUint8 = rule.detectTransferRestriction(ADDRESS1, ADDRESS2, 1); assertEq(resUint8, TRANSFER_OK); } - function testDetectRestriction_FromNotVerified() public { + /// @notice ERC-3643 mandates that ONLY the receiver be verified. An unverified SENDER is allowed + /// by default — the spec checks only the receiver precisely so a de-listed holder can + /// still exit their position. + function testDetectRestriction_UnverifiedSenderIsAllowedByDefault() public { registry.setVerified(ADDRESS2, true); + assertFalse(rule.checkSender()); + + resUint8 = rule.detectTransferRestriction(ADDRESS1, ADDRESS2, 1); + assertEq(resUint8, TRANSFER_OK); + } + + /// @notice The stricter sender check is available as an explicit opt-in. + function testDetectRestriction_FromNotVerifiedWhenCheckSenderEnabled() public { + registry.setVerified(ADDRESS2, true); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setCheckSender(true); + assertTrue(rule.checkSender()); resUint8 = rule.detectTransferRestriction(ADDRESS1, ADDRESS2, 1); assertEq(resUint8, CODE_ADDRESS_FROM_NOT_VERIFIED); @@ -43,14 +59,54 @@ contract RuleIdentityRegistryUnit is Test, HelperContract { assertEq(resUint8, TRANSFER_OK); } - function testDetectRestrictionFrom_SpenderNotVerified() public { + /// @notice ERC-3643: "`transferFrom` works the same way" — receiver only. An unverified SPENDER + /// is allowed by default. + function testDetectRestrictionFrom_UnverifiedSpenderIsAllowedByDefault() public { + registry.setVerified(ADDRESS1, true); + registry.setVerified(ADDRESS2, true); + assertFalse(rule.checkSpender()); + + resUint8 = rule.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ADDRESS2, 1); + assertEq(resUint8, TRANSFER_OK); + } + + /// @notice The stricter spender check is available as an explicit opt-in. + function testDetectRestrictionFrom_SpenderNotVerifiedWhenCheckSpenderEnabled() public { registry.setVerified(ADDRESS1, true); registry.setVerified(ADDRESS2, true); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setCheckSpender(true); + resUint8 = rule.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ADDRESS2, 1); assertEq(resUint8, CODE_ADDRESS_SPENDER_NOT_VERIFIED); } + /// @notice Even with `checkSpender` ON, a MINT exempts the spender: ERC-3643 says `mint` "only + /// requires the receiver to be whitelisted and verified". An unverified minter may mint + /// to a verified recipient. + function testMintExemptsTheMinterEvenWhenCheckSpenderEnabled() public { + registry.setVerified(ADDRESS2, true); // recipient verified; MINTER (ADDRESS3) is not + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setCheckSpender(true); + + resUint8 = rule.detectTransferRestrictionFrom(ADDRESS3, ZERO_ADDRESS, ADDRESS2, 1); + assertEq(resUint8, TRANSFER_OK); + } + + /// @notice ERC-3643: "The `burn` function bypasses all checks on eligibility." + function testBurnBypassesAllChecks() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + rule.setCheckSender(true); + rule.setCheckSpender(true); + vm.stopPrank(); + + // Nobody is verified, yet the burn passes. + assertEq(rule.detectTransferRestriction(ADDRESS1, ZERO_ADDRESS, 1), TRANSFER_OK); + assertEq(rule.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ZERO_ADDRESS, 1), TRANSFER_OK); + } + function testDetectRestrictionFrom_NoRegistry_ReturnsOk() public { vm.prank(DEFAULT_ADMIN_ADDRESS); rule.clearIdentityRegistry(); @@ -77,8 +133,9 @@ contract RuleIdentityRegistryUnit is Test, HelperContract { rule.clearIdentityRegistry(); } - function testTransferred_RevertsOnInvalidTransfer() public { - registry.setVerified(ADDRESS2, true); + /// @notice Enforcement reverts when the RECEIVER is unverified — the only ERC-3643-mandated check. + function testTransferred_RevertsWhenReceiverNotVerified() public { + registry.setVerified(ADDRESS1, true); // sender verified, receiver NOT vm.expectRevert( abi.encodeWithSelector( @@ -87,16 +144,26 @@ contract RuleIdentityRegistryUnit is Test, HelperContract { ADDRESS1, ADDRESS2, 10, - CODE_ADDRESS_FROM_NOT_VERIFIED + CODE_ADDRESS_TO_NOT_VERIFIED ) ); rule.transferred(ADDRESS1, ADDRESS2, 10); } - function testTransferredFrom_RevertsOnInvalidTransfer() public { + /// @notice Enforcement does NOT revert on an unverified sender by default (ERC-3643). + function testTransferred_DoesNotRevertOnUnverifiedSenderByDefault() public { + registry.setVerified(ADDRESS2, true); // receiver verified, sender NOT + rule.transferred(ADDRESS1, ADDRESS2, 10); // no revert + } + + /// @notice With `checkSpender` opted in, enforcement reverts on an unverified spender. + function testTransferredFrom_RevertsOnUnverifiedSpenderWhenOptedIn() public { registry.setVerified(ADDRESS1, true); registry.setVerified(ADDRESS2, true); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setCheckSpender(true); + vm.expectRevert( abi.encodeWithSelector( RuleIdentityRegistry_InvalidTransferFrom.selector, @@ -111,6 +178,14 @@ contract RuleIdentityRegistryUnit is Test, HelperContract { rule.transferred(ADDRESS3, ADDRESS1, ADDRESS2, 10); } + /// @notice By default an unverified spender is accepted (ERC-3643). + function testTransferredFrom_DoesNotRevertOnUnverifiedSpenderByDefault() public { + registry.setVerified(ADDRESS1, true); + registry.setVerified(ADDRESS2, true); + + rule.transferred(ADDRESS3, ADDRESS1, ADDRESS2, 10); // no revert + } + function testTransferred_DoesNotRevertWhenValid() public { registry.setVerified(ADDRESS1, true); registry.setVerified(ADDRESS2, true); diff --git a/test/RuleMaxTotalSupply/RuleMaxTotalSupplyRuleEngineIntegration.t.sol b/test/RuleMaxTotalSupply/RuleMaxTotalSupplyRuleEngineIntegration.t.sol index a115320..1b48e79 100644 --- a/test/RuleMaxTotalSupply/RuleMaxTotalSupplyRuleEngineIntegration.t.sol +++ b/test/RuleMaxTotalSupply/RuleMaxTotalSupplyRuleEngineIntegration.t.sol @@ -61,18 +61,19 @@ contract RuleMaxTotalSupplyRuleEngineIntegration is Test, HelperContract { assertEq(resBool, false); } + /// @notice Full-domain fuzz: `value`, `currentSupply` and `maxSupply` roam the entire uint256 + /// range, including the region where `currentSupply + value` overflows. The view must + /// return a code and never revert (regression guard for the overflow fix). function testFuzz_MaxTotalSupplyBounds(uint256 currentSupply, uint256 value, uint256 maxSupply) public { - currentSupply = bound(currentSupply, 0, type(uint256).max - 1); - value = bound(value, 0, type(uint256).max - currentSupply); - maxSupply = bound(maxSupply, 0, type(uint256).max); - vm.prank(DEFAULT_ADMIN_ADDRESS); ruleMaxTotalSupply.setMaxTotalSupply(maxSupply); token.setTotalSupply(currentSupply); resUint8 = ruleEngineMock.detectTransferRestriction(address(0), ADDRESS1, value); - if (currentSupply + value > maxSupply) { + // Overflow-safe reference computation, mirroring the rule's own logic. + bool exceeds = currentSupply > maxSupply || value > maxSupply - currentSupply; + if (exceeds) { assertEq(resUint8, CODE_MAX_TOTAL_SUPPLY_EXCEEDED); } else { assertEq(resUint8, TRANSFER_OK); diff --git a/test/RuleMintAllowance/CMTATIntegration.t.sol b/test/RuleMintAllowance/CMTATIntegration.t.sol new file mode 100644 index 0000000..00ff6c6 --- /dev/null +++ b/test/RuleMintAllowance/CMTATIntegration.t.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; +import {RuleMintAllowance} from "src/rules/operation/RuleMintAllowance.sol"; +import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; + +/** + * @title End-to-end integration test: CMTAT + RuleEngine + RuleMintAllowance + * @notice Verifies that the mint allowance is enforced through the full CMTAT call chain. + * In CMTAT v3.3+ the minter address is passed as `spender` when the rule engine + * calls `transferred(spender, address(0), to, value)`. + */ +contract CMTATIntegration is Test, HelperContract { + address constant MINTER = address(10); + + RuleMintAllowance private mintAllowanceRule; + + function setUp() public { + cmtatDeployment = new CMTATDeployment(); + cmtatContract = cmtatDeployment.cmtat(); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, address(cmtatContract)); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule = new RuleMintAllowance(DEFAULT_ADMIN_ADDRESS); + + // Bind the rule to the RuleEngine (the entity that calls transferred() on the rule) + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.bindToken(address(ruleEngineMock)); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock.addRule(mintAllowanceRule); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.setRuleEngine(ruleEngineMock); + + // Grant minter role on CMTAT to MINTER + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.grantRole(keccak256("MINTER_ROLE"), MINTER); + } + + function testMintSucceedsWithSufficientAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 500); + + vm.prank(MINTER); + cmtatContract.mint(ADDRESS1, 300); + + assertEq(cmtatContract.balanceOf(ADDRESS1), 300); + assertEq(mintAllowanceRule.mintAllowance(MINTER), 200); + } + + function testMintRevertsWhenAllowanceExceeded() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 100); + + vm.prank(MINTER); + vm.expectRevert( + abi.encodeWithSelector( + RuleMintAllowance_AllowanceExceeded.selector, address(mintAllowanceRule), MINTER, 100, 101 + ) + ); + cmtatContract.mint(ADDRESS1, 101); + } + + function testMintRevertsWithZeroAllowance() public { + // Default allowance is 0 + vm.prank(MINTER); + vm.expectRevert(); + cmtatContract.mint(ADDRESS1, 1); + } + + function testBatchMintSucceedsWithinCumulativeAllowance() public { + address[] memory accounts = new address[](2); + accounts[0] = ADDRESS1; + accounts[1] = ADDRESS2; + + uint256[] memory values = new uint256[](2); + values[0] = 300; + values[1] = 200; + + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 500); + + vm.prank(MINTER); + cmtatContract.batchMint(accounts, values); + + assertEq(cmtatContract.balanceOf(ADDRESS1), 300); + assertEq(cmtatContract.balanceOf(ADDRESS2), 200); + assertEq(mintAllowanceRule.mintAllowance(MINTER), 0); + } + + function testBatchMintRevertsAndRollsBackWhenCumulativeAllowanceExceeded() public { + address[] memory accounts = new address[](2); + accounts[0] = ADDRESS1; + accounts[1] = ADDRESS2; + + uint256[] memory values = new uint256[](2); + values[0] = 300; + values[1] = 201; + + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 500); + + vm.prank(MINTER); + vm.expectRevert( + abi.encodeWithSelector( + RuleMintAllowance_AllowanceExceeded.selector, address(mintAllowanceRule), MINTER, 200, 201 + ) + ); + cmtatContract.batchMint(accounts, values); + + assertEq(cmtatContract.balanceOf(ADDRESS1), 0); + assertEq(cmtatContract.balanceOf(ADDRESS2), 0); + assertEq(mintAllowanceRule.mintAllowance(MINTER), 500); + } + + function testMultipleMintersSeparateAllowances() public { + address minter2 = address(11); + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.grantRole(keccak256("MINTER_ROLE"), minter2); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 500); + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(minter2, 200); + + vm.prank(MINTER); + cmtatContract.mint(ADDRESS1, 500); + vm.prank(minter2); + cmtatContract.mint(ADDRESS2, 200); + + assertEq(mintAllowanceRule.mintAllowance(MINTER), 0); + assertEq(mintAllowanceRule.mintAllowance(minter2), 0); + assertEq(cmtatContract.balanceOf(ADDRESS1), 500); + assertEq(cmtatContract.balanceOf(ADDRESS2), 200); + } + + function testDetectTransferRestrictionFromViaCMTAT() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 100); + + // Sufficient allowance + assertEq(cmtatContract.detectTransferRestrictionFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 100), TRANSFER_OK); + assertTrue(cmtatContract.canTransferFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 100)); + + // Exceeds allowance + assertEq( + cmtatContract.detectTransferRestrictionFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 101), + CODE_MINTER_ALLOWANCE_EXCEEDED + ); + assertFalse(cmtatContract.canTransferFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 101)); + } + + function testRegularTransfersAreNotRestricted() public { + // Set up balances via admin mint (admin has unlimited system-level access before rule kicks in) + // We need to give admin an allowance to mint first + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(DEFAULT_ADMIN_ADDRESS, 1000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.mint(ADDRESS1, 500); + + // Regular transfer should not be restricted by mint allowance rule + assertTrue(cmtatContract.canTransfer(ADDRESS1, ADDRESS2, 100)); + assertEq(cmtatContract.detectTransferRestriction(ADDRESS1, ADDRESS2, 100), TRANSFER_OK); + + vm.prank(ADDRESS1); + // forge-lint: disable-next-line(erc20-unchecked-transfer) + cmtatContract.transfer(ADDRESS2, 100); + assertEq(cmtatContract.balanceOf(ADDRESS2), 100); + } + + function testAllowanceCanBeIncreasedAfterExhaustion() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.setMintAllowance(MINTER, 100); + + vm.prank(MINTER); + cmtatContract.mint(ADDRESS1, 100); + assertEq(mintAllowanceRule.mintAllowance(MINTER), 0); + + // Next mint fails + vm.prank(MINTER); + vm.expectRevert(); + cmtatContract.mint(ADDRESS1, 1); + + // Operator increases allowance + vm.prank(DEFAULT_ADMIN_ADDRESS); + mintAllowanceRule.increaseMintAllowance(MINTER, 50); + vm.prank(MINTER); + cmtatContract.mint(ADDRESS1, 50); + assertEq(mintAllowanceRule.mintAllowance(MINTER), 0); + } +} diff --git a/test/RuleMintAllowance/RuleMintAllowance.t.sol b/test/RuleMintAllowance/RuleMintAllowance.t.sol new file mode 100644 index 0000000..93e3c71 --- /dev/null +++ b/test/RuleMintAllowance/RuleMintAllowance.t.sol @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {RuleMintAllowance} from "src/rules/operation/RuleMintAllowance.sol"; +import {AccessControlModuleStandalone} from "src/modules/AccessControlModuleStandalone.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {IERC3643ComplianceFull} from "src/mocks/IERC3643ComplianceFull.sol"; + +contract RuleMintAllowanceTest is Test, HelperContract { + uint8 internal constant CODE_ALLOWANCE_EXCEEDED = 70; + string internal constant TEXT_ALLOWANCE_EXCEEDED = "MintAllowance: minter allowance exceeded"; + + address constant OPERATOR = address(10); + address constant MINTER = address(11); + address constant BOUND_ENGINE = address(12); + + RuleMintAllowance private rule; + + function setUp() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule = new RuleMintAllowance(DEFAULT_ADMIN_ADDRESS); + // Bind a fake engine so transferred() calls succeed + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(BOUND_ENGINE); + } + + /*////////////////////////////////////////////////////////////// + DEPLOYMENT + //////////////////////////////////////////////////////////////*/ + + function testCannotDeployWithZeroAdmin() public { + vm.expectRevert(AccessControlModuleStandalone.AccessControlModuleStandalone_AddressZeroNotAllowed.selector); + new RuleMintAllowance(ZERO_ADDRESS); + } + + /*////////////////////////////////////////////////////////////// + ALLOWANCE MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + function testSetMintAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + assertEq(rule.mintAllowance(MINTER), 1000); + } + + function testSetMintAllowanceEmitsEvent() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectEmit(true, false, false, true); + emit MintAllowanceSet(MINTER, 1000); + rule.setMintAllowance(MINTER, 1000); + } + + function testSetMintAllowanceOverridesExisting() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 500); + assertEq(rule.mintAllowance(MINTER), 500); + } + + function testIncreaseMintAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 500); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.increaseMintAllowance(MINTER, 300); + assertEq(rule.mintAllowance(MINTER), 800); + } + + function testIncreaseMintAllowanceEmitsEvent() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 500); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectEmit(true, false, false, true); + emit MintAllowanceIncreased(MINTER, 300, 800); + rule.increaseMintAllowance(MINTER, 300); + } + + function testDecreaseMintAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.decreaseMintAllowance(MINTER, 400); + assertEq(rule.mintAllowance(MINTER), 600); + } + + function testDecreaseMintAllowanceEmitsEvent() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectEmit(true, false, false, true); + emit MintAllowanceDecreased(MINTER, 400, 600); + rule.decreaseMintAllowance(MINTER, 400); + } + + function testDecreaseMintAllowanceRevertsOnUnderflow() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectRevert(abi.encodeWithSelector(RuleMintAllowance_DecreaseBelowZero.selector, MINTER, 100, 200)); + rule.decreaseMintAllowance(MINTER, 200); + } + + function testDecreaseMintAllowanceToExactZero() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.decreaseMintAllowance(MINTER, 100); + assertEq(rule.mintAllowance(MINTER), 0); + } + + /*////////////////////////////////////////////////////////////// + ACCESS CONTROL + //////////////////////////////////////////////////////////////*/ + + function testOnlyOperatorCanSetAllowance() public { + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.setMintAllowance(MINTER, 1000); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.grantRole(ALLOWANCE_OPERATOR_ROLE, OPERATOR); + vm.prank(OPERATOR); + rule.setMintAllowance(MINTER, 1000); + assertEq(rule.mintAllowance(MINTER), 1000); + } + + function testOnlyOperatorCanIncrease() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.increaseMintAllowance(MINTER, 50); + } + + function testOnlyOperatorCanDecrease() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.decreaseMintAllowance(MINTER, 50); + } + + function testDefaultAdminHasOperatorRole() public { + assertTrue(rule.hasRole(ALLOWANCE_OPERATOR_ROLE, DEFAULT_ADMIN_ADDRESS)); + } + + /*////////////////////////////////////////////////////////////// + DETECT TRANSFER RESTRICTION + //////////////////////////////////////////////////////////////*/ + + function testDetectTransferRestrictionAlwaysOk() public view { + assertEq(rule.detectTransferRestriction(ADDRESS1, ADDRESS2, 100), TRANSFER_OK); + assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS2, 100), TRANSFER_OK); + } + + function testCanTransferAlwaysTrue() public view { + assertTrue(rule.canTransfer(ADDRESS1, ADDRESS2, 100)); + assertTrue(rule.canTransfer(ZERO_ADDRESS, ADDRESS2, 100)); + } + + function testDetectTransferRestrictionFromMintWithSufficientAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 500); + assertEq(rule.detectTransferRestrictionFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 500), TRANSFER_OK); + assertTrue(rule.canTransferFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 500)); + } + + function testDetectTransferRestrictionFromMintExceedsAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + assertEq(rule.detectTransferRestrictionFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 101), CODE_ALLOWANCE_EXCEEDED); + assertFalse(rule.canTransferFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 101)); + } + + function testDetectTransferRestrictionFromRegularTransferAlwaysOk() public view { + // Non-mint transfers are always OK regardless of allowance + assertEq(rule.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ADDRESS2, 10000), TRANSFER_OK); + assertTrue(rule.canTransferFrom(ADDRESS3, ADDRESS1, ADDRESS2, 10000)); + } + + function testDetectTransferRestrictionFromBurnAlwaysOk() public view { + // Burns (to == address(0)) are not restricted + assertEq(rule.detectTransferRestrictionFrom(ADDRESS1, ADDRESS1, ZERO_ADDRESS, 10000), TRANSFER_OK); + assertTrue(rule.canTransferFrom(ADDRESS1, ADDRESS1, ZERO_ADDRESS, 10000)); + } + + function testZeroAllowanceMintBlocked() public view { + // Default allowance is 0; any mint amount should be blocked + assertEq(rule.detectTransferRestrictionFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 1), CODE_ALLOWANCE_EXCEEDED); + assertFalse(rule.canTransferFrom(MINTER, ZERO_ADDRESS, ADDRESS1, 1)); + } + + /*////////////////////////////////////////////////////////////// + TRANSFERRED (STATE) + //////////////////////////////////////////////////////////////*/ + + function testTransferredFourArgConsumesAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.prank(BOUND_ENGINE); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 400); + assertEq(rule.mintAllowance(MINTER), 600); + } + + function testTransferredFourArgEmitsConsumedEvent() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.prank(BOUND_ENGINE); + vm.expectEmit(true, false, false, true); + emit MintAllowanceConsumed(MINTER, 400, 600); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 400); + } + + function testTransferredFourArgRevertsOnExceedAllowance() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + vm.prank(BOUND_ENGINE); + vm.expectRevert( + abi.encodeWithSelector(RuleMintAllowance_AllowanceExceeded.selector, address(rule), MINTER, 100, 101) + ); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 101); + } + + function testTransferredFourArgRegularTransferNoOp() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + // Regular transfer (from != address(0)) should not affect allowance + vm.prank(BOUND_ENGINE); + rule.transferred(ADDRESS3, ADDRESS1, ADDRESS2, 500); + assertEq(rule.mintAllowance(MINTER), 1000); + } + + function testTransferredThreeArgNoOp() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + // 3-arg path: no deduction even for from == address(0) + vm.prank(BOUND_ENGINE); + rule.transferred(ZERO_ADDRESS, ADDRESS1, 500); + assertEq(rule.mintAllowance(MINTER), 1000); + } + + function testTransferredOnlyBoundTokenCan3Arg() public { + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.transferred(ADDRESS1, ADDRESS2, 10); + } + + function testTransferredOnlyBoundTokenCan4Arg() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 100); + } + + function testMultipleMintConsumesSequentially() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + + vm.prank(BOUND_ENGINE); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 300); + assertEq(rule.mintAllowance(MINTER), 700); + + vm.prank(BOUND_ENGINE); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS2, 700); + assertEq(rule.mintAllowance(MINTER), 0); + + // Next mint must fail + vm.prank(BOUND_ENGINE); + vm.expectRevert(); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 1); + } + + function testSetAllowanceResetsAfterExhaustion() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 100); + vm.prank(BOUND_ENGINE); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 100); + assertEq(rule.mintAllowance(MINTER), 0); + + // Operator resets the allowance + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 200); + assertEq(rule.mintAllowance(MINTER), 200); + + vm.prank(BOUND_ENGINE); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 200); + assertEq(rule.mintAllowance(MINTER), 0); + } + + /*////////////////////////////////////////////////////////////// + RESTRICTION CODE & MESSAGE + //////////////////////////////////////////////////////////////*/ + + function testCanReturnRestrictionCode() public view { + assertTrue(rule.canReturnTransferRestrictionCode(CODE_ALLOWANCE_EXCEEDED)); + assertFalse(rule.canReturnTransferRestrictionCode(CODE_NONEXISTENT)); + } + + function testMessageForTransferRestriction() public view { + assertEq(rule.messageForTransferRestriction(CODE_ALLOWANCE_EXCEEDED), TEXT_ALLOWANCE_EXCEEDED); + assertEq(rule.messageForTransferRestriction(CODE_NONEXISTENT), TEXT_CODE_NOT_FOUND); + } + + /*////////////////////////////////////////////////////////////// + ERC-165 + //////////////////////////////////////////////////////////////*/ + + function testSupportsInterface() public view { + assertTrue(rule.supportsInterface(type(IAccessControl).interfaceId)); + assertTrue(rule.supportsInterface(RuleInterfaceId.IRULE_INTERFACE_ID)); + assertTrue(rule.supportsInterface(ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID)); + assertTrue(rule.supportsInterface(RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID)); + assertTrue(rule.supportsInterface(type(IERC7551Compliance).interfaceId)); + assertFalse(rule.supportsInterface(type(IERC3643ComplianceFull).interfaceId)); + assertFalse(rule.supportsInterface(bytes4(0xdeadbeef))); + } + + /*////////////////////////////////////////////////////////////// + COMPLIANCE MODULE (BINDING) + //////////////////////////////////////////////////////////////*/ + + function testBindTokenOnlyComplianceManager() public { + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.bindToken(ADDRESS2); + + vm.expectRevert(RuleMintAllowance_TokenAlreadyBound.selector); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS2); + } + + function testBindTokenAfterUnbind() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.unbindToken(BOUND_ENGINE); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS2); + assertTrue(rule.isTokenBound(ADDRESS2)); + } + + function testUnbindToken() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.unbindToken(BOUND_ENGINE); + assertFalse(rule.isTokenBound(BOUND_ENGINE)); + } + + function testCreatedAndDestroyedAreNoOps() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.prank(BOUND_ENGINE); + rule.created(ADDRESS1, 500); + assertEq(rule.mintAllowance(MINTER), 1000); + + vm.prank(BOUND_ENGINE); + rule.destroyed(ADDRESS1, 300); + assertEq(rule.mintAllowance(MINTER), 1000); + } +} diff --git a/test/RuleMintAllowance/RuleMintAllowanceOwnable2Step.t.sol b/test/RuleMintAllowance/RuleMintAllowanceOwnable2Step.t.sol new file mode 100644 index 0000000..564dea0 --- /dev/null +++ b/test/RuleMintAllowance/RuleMintAllowanceOwnable2Step.t.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {RuleMintAllowanceOwnable2Step} from "src/rules/operation/RuleMintAllowanceOwnable2Step.sol"; +import {RuleInterfaceId} from "RuleEngine/modules/library/RuleInterfaceId.sol"; +import {ERC1404ExtendInterfaceId} from "CMTAT/library/ERC1404ExtendInterfaceId.sol"; +import {RuleEngineInterfaceId} from "CMTAT/library/RuleEngineInterfaceId.sol"; +import {OwnableInterfaceId} from "RuleEngine/modules/library/OwnableInterfaceId.sol"; +import {Ownable2StepInterfaceId} from "RuleEngine/modules/library/Ownable2StepInterfaceId.sol"; +import {IERC7551Compliance} from "CMTAT/interfaces/tokenization/draft-IERC7551.sol"; +import {IERC3643ComplianceFull} from "src/mocks/IERC3643ComplianceFull.sol"; + +contract RuleMintAllowanceOwnable2StepTest is Test, HelperContract { + address constant OWNER = address(1); + address constant MINTER = address(11); + address constant BOUND_ENGINE = address(12); + + RuleMintAllowanceOwnable2Step private rule; + + function setUp() public { + vm.prank(OWNER); + rule = new RuleMintAllowanceOwnable2Step(OWNER); + vm.prank(OWNER); + rule.bindToken(BOUND_ENGINE); + } + + function testOwnerCanSetAllowance() public { + vm.prank(OWNER); + rule.setMintAllowance(MINTER, 500); + assertEq(rule.mintAllowance(MINTER), 500); + } + + function testNonOwnerCannotSetAllowance() public { + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.setMintAllowance(MINTER, 500); + } + + function testOwnerCanIncreaseAndDecrease() public { + vm.prank(OWNER); + rule.setMintAllowance(MINTER, 1000); + vm.prank(OWNER); + rule.increaseMintAllowance(MINTER, 200); + assertEq(rule.mintAllowance(MINTER), 1200); + vm.prank(OWNER); + rule.decreaseMintAllowance(MINTER, 300); + assertEq(rule.mintAllowance(MINTER), 900); + } + + function testOwnerCanBindUnbind() public { + vm.expectRevert(RuleMintAllowance_TokenAlreadyBound.selector); + vm.prank(OWNER); + rule.bindToken(ADDRESS2); + + vm.prank(OWNER); + rule.unbindToken(BOUND_ENGINE); + + vm.prank(OWNER); + rule.bindToken(ADDRESS2); + assertTrue(rule.isTokenBound(ADDRESS2)); + vm.prank(OWNER); + rule.unbindToken(ADDRESS2); + assertFalse(rule.isTokenBound(ADDRESS2)); + } + + function testNonOwnerCannotBind() public { + vm.expectRevert(); + vm.prank(ADDRESS1); + rule.bindToken(ADDRESS2); + } + + function testTransferredConsumesAllowance() public { + vm.prank(OWNER); + rule.setMintAllowance(MINTER, 1000); + vm.prank(BOUND_ENGINE); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 300); + assertEq(rule.mintAllowance(MINTER), 700); + } + + function testOwnershipTransferTwoStep() public { + vm.prank(OWNER); + rule.transferOwnership(ADDRESS1); + assertEq(rule.pendingOwner(), ADDRESS1); + assertEq(rule.owner(), OWNER); + + vm.prank(ADDRESS1); + rule.acceptOwnership(); + assertEq(rule.owner(), ADDRESS1); + } + + function testSupportsInterface() public view { + assertTrue(rule.supportsInterface(OwnableInterfaceId.IERC173_INTERFACE_ID)); + assertTrue(rule.supportsInterface(Ownable2StepInterfaceId.IOWNABLE2STEP_INTERFACE_ID)); + assertTrue(rule.supportsInterface(RuleInterfaceId.IRULE_INTERFACE_ID)); + assertTrue(rule.supportsInterface(ERC1404ExtendInterfaceId.ERC1404EXTEND_INTERFACE_ID)); + assertTrue(rule.supportsInterface(RuleEngineInterfaceId.RULE_ENGINE_INTERFACE_ID)); + assertTrue(rule.supportsInterface(type(IERC7551Compliance).interfaceId)); + assertFalse(rule.supportsInterface(type(IERC3643ComplianceFull).interfaceId)); + assertFalse(rule.supportsInterface(bytes4(0xdeadbeef))); + } +} diff --git a/test/RuleSanctionList/RuleSanctionListTest.t.sol b/test/RuleSanctionList/RuleSanctionListTest.t.sol index aeacda7..4c48b18 100644 --- a/test/RuleSanctionList/RuleSanctionListTest.t.sol +++ b/test/RuleSanctionList/RuleSanctionListTest.t.sol @@ -193,6 +193,23 @@ contract RuleSanctionlistTest is Test, HelperContract { ruleSanctionList.transferred(ADDRESS1, ADDRESS2, 0, 20); } + function testRemoveFromSanctionsListUnblocksTransfer() public { + // Arrange: ATTACKER is sanctioned in setUp, so the rule blocks the transfer + assertEq(sanctionlistOracle.isSanctioned(ATTACKER), true); + resUint8 = ruleSanctionList.detectTransferRestriction(ATTACKER, ADDRESS2, 20); + assertEq(resUint8, CODE_ADDRESS_FROM_IS_SANCTIONED); + + // Act: remove ATTACKER from the sanctions list + sanctionlistOracle.removeFromSanctionsList(ATTACKER); + + // Assert: no longer sanctioned and the transfer now passes + assertEq(sanctionlistOracle.isSanctioned(ATTACKER), false); + resUint8 = ruleSanctionList.detectTransferRestriction(ATTACKER, ADDRESS2, 20); + assertEq(resUint8, NO_ERROR); + // No revert + ruleSanctionList.transferred(ATTACKER, ADDRESS2, 20); + } + function testDetectTransferRestrictionWitSpenderOk() public { // Act resUint8 = ruleSanctionList.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ADDRESS2, 20); diff --git a/test/RuleSpenderWhitelist/RuleSpenderWhitelist.t.sol b/test/RuleSpenderWhitelist/RuleSpenderWhitelist.t.sol index 075475b..e1aaad1 100644 --- a/test/RuleSpenderWhitelist/RuleSpenderWhitelist.t.sol +++ b/test/RuleSpenderWhitelist/RuleSpenderWhitelist.t.sol @@ -40,6 +40,20 @@ contract RuleSpenderWhitelistTest is Test, HelperContract { rule.transferred(ADDRESS3, ADDRESS1, ADDRESS2, 10); } + function testMintExemptFromSpenderCheck() public { + // Minter (spender) not whitelisted — mint must still succeed because from == address(0). + assertEq(rule.detectTransferRestrictionFrom(ADDRESS3, ZERO_ADDRESS, ADDRESS2, 10), TRANSFER_OK); + assertTrue(rule.canTransferFrom(ADDRESS3, ZERO_ADDRESS, ADDRESS2, 10)); + rule.transferred(ADDRESS3, ZERO_ADDRESS, ADDRESS2, 10); + } + + function testBurnExemptFromSpenderCheck() public { + // Burner (spender) not whitelisted — burn must still succeed because to == address(0). + assertEq(rule.detectTransferRestrictionFrom(ADDRESS3, ADDRESS1, ZERO_ADDRESS, 10), TRANSFER_OK); + assertTrue(rule.canTransferFrom(ADDRESS3, ADDRESS1, ZERO_ADDRESS, 10)); + rule.transferred(ADDRESS3, ADDRESS1, ZERO_ADDRESS, 10); + } + function testTransferFromAllowedWhenSpenderWhitelisted() public { vm.prank(DEFAULT_ADMIN_ADDRESS); rule.addAddress(ADDRESS3); diff --git a/test/RuleWhitelist/AccessControl/RuleWhitelistWrapperAccessControlRoleMembers.t.sol b/test/RuleWhitelist/AccessControl/RuleWhitelistWrapperAccessControlRoleMembers.t.sol index 7103698..bb60a2a 100644 --- a/test/RuleWhitelist/AccessControl/RuleWhitelistWrapperAccessControlRoleMembers.t.sol +++ b/test/RuleWhitelist/AccessControl/RuleWhitelistWrapperAccessControlRoleMembers.t.sol @@ -10,7 +10,7 @@ import {RuleWhitelistWrapper} from "src/rules/validation/deployment/RuleWhitelis contract RuleWhitelistWrapperAccessControlRoleMembers is AccessControlEnumerableTestBase { function _deployAccessControl() internal override returns (IAccessControlEnumerableLike, address) { address adminAddr = DEFAULT_ADMIN_ADDRESS; - RuleWhitelistWrapper rule = new RuleWhitelistWrapper(adminAddr, ZERO_ADDRESS, true); + RuleWhitelistWrapper rule = new RuleWhitelistWrapper(adminAddr, ZERO_ADDRESS, true, true); return (IAccessControlEnumerableLike(address(rule)), adminAddr); } diff --git a/test/RuleWhitelist/CMTATIntegration.t.sol b/test/RuleWhitelist/CMTATIntegration.t.sol index c861294..f09992f 100644 --- a/test/RuleWhitelist/CMTATIntegration.t.sol +++ b/test/RuleWhitelist/CMTATIntegration.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {HelperContract} from "../HelperContract.sol"; -import {CMTATDeployment} from "RuleEngine/../test/utils/CMTATDeployment.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; @@ -195,14 +195,14 @@ contract CMTATIntegration is Test, HelperContract { function testCanMint() public { // Arrange - // Add address zero to the whitelist + // Permit minting via the explicit flag (the zero address is never whitelisted) vm.prank(DEFAULT_ADMIN_ADDRESS); - ruleWhitelist.addAddress(ZERO_ADDRESS); + ruleWhitelist.setAllowMint(true); vm.prank(DEFAULT_ADMIN_ADDRESS); ruleWhitelist.addAddress(ADDRESS1); - // Arrange - Assert - resBool = ruleWhitelist.isAddressListed(ZERO_ADDRESS); - assertEq(resBool, true); + // Arrange - Assert: the sentinel is NOT listed; the explicit flag is what permits the mint. + assertFalse(ruleWhitelist.isAddressListed(ZERO_ADDRESS)); + assertTrue(ruleWhitelist.allowMint()); // Act vm.prank(DEFAULT_ADMIN_ADDRESS); diff --git a/test/RuleWhitelist/CMTATIntegrationWhitelistWrapper.t.sol b/test/RuleWhitelist/CMTATIntegrationWhitelistWrapper.t.sol index a102d90..96c791b 100644 --- a/test/RuleWhitelist/CMTATIntegrationWhitelistWrapper.t.sol +++ b/test/RuleWhitelist/CMTATIntegrationWhitelistWrapper.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {HelperContract} from "../HelperContract.sol"; -import {CMTATDeployment} from "RuleEngine/../test/utils/CMTATDeployment.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; import {RuleWhitelistWrapper} from "src/rules/validation/deployment/RuleWhitelistWrapper.sol"; import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; @@ -27,7 +27,7 @@ contract CMTATIntegrationWhitelistWrapper is Test, HelperContract { ruleWhitelist = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, false); ruleWhitelist2 = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, false); ruleWhitelist3 = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, false); - ruleWhitelistWrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true); + ruleWhitelistWrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, true); vm.prank(DEFAULT_ADMIN_ADDRESS); ruleWhitelistWrapper.addRule(ruleWhitelist); vm.prank(DEFAULT_ADMIN_ADDRESS); @@ -59,7 +59,7 @@ contract CMTATIntegrationWhitelistWrapper is Test, HelperContract { function testCannotDeployContractIfAdminAddressIsZero() public { vm.prank(WHITELIST_OPERATOR_ADDRESS); vm.expectRevert(AccessControlModuleStandalone.AccessControlModuleStandalone_AddressZeroNotAllowed.selector); - ruleWhitelistWrapper = new RuleWhitelistWrapper(ZERO_ADDRESS, ZERO_ADDRESS, true); + ruleWhitelistWrapper = new RuleWhitelistWrapper(ZERO_ADDRESS, ZERO_ADDRESS, true, true); } /** @@ -272,14 +272,17 @@ contract CMTATIntegrationWhitelistWrapper is Test, HelperContract { function testCanMint() public { // Arrange - // Add address zero to the whitelist + // Permit minting via the explicit flag on the WRAPPER (the zero address is never whitelisted) vm.prank(DEFAULT_ADMIN_ADDRESS); - ruleWhitelist.addAddress(ZERO_ADDRESS); + ruleWhitelistWrapper.setAllowMint(true); vm.prank(DEFAULT_ADMIN_ADDRESS); ruleWhitelist.addAddress(ADDRESS1); - // Arrange - Assert - resBool = ruleWhitelist.isAddressListed(ZERO_ADDRESS); - assertEq(resBool, true); + // Arrange - Assert: the sentinel is NOT listed in the child; the WRAPPER's explicit flag is + // what permits the mint, and the recipient must still be listed in some child. + assertFalse(ruleWhitelist.isAddressListed(ZERO_ADDRESS)); + assertTrue(ruleWhitelistWrapper.allowMint()); + assertTrue(ruleWhitelistWrapper.isVerified(ADDRESS1)); + assertFalse(ruleWhitelistWrapper.isVerified(ZERO_ADDRESS)); // Act vm.prank(DEFAULT_ADMIN_ADDRESS); diff --git a/test/RuleWhitelist/CMTATRuleEngineIntegration.t.sol b/test/RuleWhitelist/CMTATRuleEngineIntegration.t.sol index 6a5794d..d072c1b 100644 --- a/test/RuleWhitelist/CMTATRuleEngineIntegration.t.sol +++ b/test/RuleWhitelist/CMTATRuleEngineIntegration.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {HelperContract} from "../HelperContract.sol"; -import {CMTATDeployment} from "RuleEngine/../test/utils/CMTATDeployment.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; @@ -24,9 +24,10 @@ contract CMTATRuleEngineIntegration is Test, HelperContract { ruleEngineMock = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, address(cmtatContract)); vm.prank(DEFAULT_ADMIN_ADDRESS); ruleEngineMock.addRule(ruleWhitelist); - // Allow minting: whitelist ZERO_ADDRESS (mint source) and recipient. + // Allow minting: set the explicit flag (the zero address is NEVER whitelisted) and + // whitelist the recipient, which a permitted mint still requires. vm.prank(DEFAULT_ADMIN_ADDRESS); - ruleWhitelist.addAddress(ZERO_ADDRESS); + ruleWhitelist.setAllowMint(true); vm.prank(DEFAULT_ADMIN_ADDRESS); ruleWhitelist.addAddress(ADDRESS1); diff --git a/test/RuleWhitelist/Ownable/RuleWhitelistOwnableAccessControl.t.sol b/test/RuleWhitelist/Ownable/RuleWhitelistOwnableAccessControl.t.sol index f3c95e7..7bc43d6 100644 --- a/test/RuleWhitelist/Ownable/RuleWhitelistOwnableAccessControl.t.sol +++ b/test/RuleWhitelist/Ownable/RuleWhitelistOwnableAccessControl.t.sol @@ -12,9 +12,14 @@ contract RuleWhitelistOwnable2StepAccessControl is OwnableAddressListTestBase { return (IAddressList(address(rule)), ownerAddr); } - function testAllowMintBurnConstructorListsZeroAddress() public { + /// @notice `allowMintBurn` sets the explicit flags; the zero address is never listed. + function testAllowMintBurnConstructorSetsFlagsAndDoesNotListZeroAddress() public { address ownerAddr = WHITELIST_OPERATOR_ADDRESS; RuleWhitelistOwnable2Step rule = new RuleWhitelistOwnable2Step(ownerAddr, ZERO_ADDRESS, true, true); - assertTrue(rule.isAddressListed(ZERO_ADDRESS)); + + assertTrue(rule.allowMint()); + assertTrue(rule.allowBurn()); + assertFalse(rule.isAddressListed(ZERO_ADDRESS)); + assertFalse(rule.isVerified(ZERO_ADDRESS)); } } diff --git a/test/RuleWhitelist/Ownable/RuleWhitelistWrapperOwnable2Step.t.sol b/test/RuleWhitelist/Ownable/RuleWhitelistWrapperOwnable2Step.t.sol index 3b21ebe..397f434 100644 --- a/test/RuleWhitelist/Ownable/RuleWhitelistWrapperOwnable2Step.t.sol +++ b/test/RuleWhitelist/Ownable/RuleWhitelistWrapperOwnable2Step.t.sol @@ -11,7 +11,7 @@ contract RuleWhitelistWrapperOwnable2StepTest is Ownable2StepTestBase { function _deployOwnable2Step() internal override returns (IOwnable2StepLike, address) { address ownerAddr = WHITELIST_OPERATOR_ADDRESS; - wrapper = new RuleWhitelistWrapperOwnable2Step(ownerAddr, ZERO_ADDRESS, true); + wrapper = new RuleWhitelistWrapperOwnable2Step(ownerAddr, ZERO_ADDRESS, true, true); rule = new RuleWhitelist(ownerAddr, ZERO_ADDRESS, true, false); return (IOwnable2StepLike(address(wrapper)), ownerAddr); } diff --git a/test/RuleWhitelist/RuleWhitelist.t.sol b/test/RuleWhitelist/RuleWhitelist.t.sol index 40f7cb7..99061e2 100644 --- a/test/RuleWhitelist/RuleWhitelist.t.sol +++ b/test/RuleWhitelist/RuleWhitelist.t.sol @@ -64,10 +64,50 @@ contract RuleWhitelistTest is Test, HelperContract { assertEq(resBool, true); } - function testAllowMintBurnConstructorListsZeroAddress() public { + /// @notice `allowMintBurn` sets the explicit flags and NEVER lists the zero address, so the + /// standardized identity getters stay truthful (ERC-3643: `isVerified` means + /// "valid investor holding the required claims"; the zero address is neither). + function testAllowMintBurnConstructorSetsFlagsAndDoesNotListZeroAddress() public { vm.prank(WHITELIST_OPERATOR_ADDRESS); - RuleWhitelist burnEnabledRule = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true, true); - assertTrue(burnEnabledRule.isAddressListed(ZERO_ADDRESS)); + RuleWhitelist mintBurnEnabled = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true, true); + + assertTrue(mintBurnEnabled.allowMint()); + assertTrue(mintBurnEnabled.allowBurn()); + + // The sentinel is NOT a list member, and the getters say so. + assertFalse(mintBurnEnabled.isAddressListed(ZERO_ADDRESS)); + assertFalse(mintBurnEnabled.isVerified(ZERO_ADDRESS)); + assertFalse(mintBurnEnabled.contains(ZERO_ADDRESS)); + assertEq(mintBurnEnabled.listedAddressCount(), 0); + } + + /// @notice With `allowMintBurn = false` the operations are refused with dedicated codes. + function testMintBurnDisabledReturnsDedicatedCodes() public { + vm.startPrank(WHITELIST_OPERATOR_ADDRESS); + RuleWhitelist noMintBurn = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, false, false); + noMintBurn.addAddress(ADDRESS1); + vm.stopPrank(); + + assertFalse(noMintBurn.allowMint()); + assertFalse(noMintBurn.allowBurn()); + assertEq(noMintBurn.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, 10), CODE_MINT_NOT_ALLOWED); + assertEq(noMintBurn.detectTransferRestriction(ADDRESS1, ZERO_ADDRESS, 10), CODE_BURN_NOT_ALLOWED); + } + + /// @notice The flags are independently settable at runtime (e.g. close issuance, keep redemptions). + function testAllowMintAndAllowBurnAreIndependentlySettable() public { + vm.startPrank(WHITELIST_OPERATOR_ADDRESS); + RuleWhitelist rule = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, false, true); + rule.addAddress(ADDRESS1); + + // Permanently close issuance, still allow redemption. + rule.setAllowMint(false); + vm.stopPrank(); + + assertFalse(rule.allowMint()); + assertTrue(rule.allowBurn()); + assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, 10), CODE_MINT_NOT_ALLOWED); + assertEq(rule.detectTransferRestriction(ADDRESS1, ZERO_ADDRESS, 10), TRANSFER_OK); } function testContainsReflectsListingStatus() public { diff --git a/test/RuleWhitelist/RuleWhitelistAdd.t.sol b/test/RuleWhitelist/RuleWhitelistAdd.t.sol index a0bbf02..fe61ba3 100644 --- a/test/RuleWhitelist/RuleWhitelistAdd.t.sol +++ b/test/RuleWhitelist/RuleWhitelistAdd.t.sol @@ -70,49 +70,54 @@ contract RuleWhitelistAddTest is Test, HelperContract { assertEq(resUint256, 2); } - function testCanAddAddressZeroToTheWhitelist() public { - // Arrange - Assert - resBool = ruleWhitelist.isAddressListed(address(0x0)); - assertEq(resBool, false); + /// @notice The zero address is the mint/burn sentinel, not a participant: it can never be listed. + /// Mint/burn permission is governed by the explicit `allowMint` / `allowBurn` flags. + function testCannotAddAddressZeroToTheWhitelist() public { + assertFalse(ruleWhitelist.isAddressListed(address(0x0))); - // Act vm.prank(WHITELIST_OPERATOR_ADDRESS); - emit IAddressList.AddAddress(address(0x0)); + vm.expectRevert(RuleAddressSet_ZeroAddressNotAllowed.selector); ruleWhitelist.addAddress(address(0x0)); - // Assert - resBool = ruleWhitelist.isAddressListed(address(0x0)); - assertEq(resBool, true); + assertFalse(ruleWhitelist.isAddressListed(address(0x0))); + assertEq(ruleWhitelist.listedAddressCount(), 0); } - function testCanAddAddressesZeroToTheWhitelist() public { - // Arrange - resUint256 = ruleWhitelist.listedAddressCount(); - assertEq(resUint256, 0); + /// @notice Batch add REJECTS the zero address rather than skipping it. Skipping would make the + /// emitted `AddAddresses` event report a member that is not in the set — re-polluting the + /// off-chain view the sentinel guard exists to keep clean. A duplicate is skipped (the + /// event still describes it truthfully); the sentinel is not. + function testAddAddressesRevertsOnZeroAddress() public { + assertEq(ruleWhitelist.listedAddressCount(), 0); address[] memory whitelist = new address[](3); whitelist[0] = ADDRESS1; whitelist[1] = ADDRESS2; - // our target address whitelist[2] = address(0x0); - // Act vm.prank(WHITELIST_OPERATOR_ADDRESS); - emit IAddressList.AddAddresses(whitelist); - (resCallBool,) = address(ruleWhitelist).call(abi.encodeWithSignature("addAddresses(address[])", whitelist)); + vm.expectRevert(RuleAddressSet_ZeroAddressNotAllowed.selector); + ruleWhitelist.addAddresses(whitelist); - // Assert - Main - // Seem that call returns true even if the function is reverted - // TODO : check the return value of call - resBool = ruleWhitelist.isAddressListed(address(0x0)); - assertEq(resBool, true); + // Nothing was listed: the whole batch reverted. + assertEq(ruleWhitelist.listedAddressCount(), 0); + assertFalse(ruleWhitelist.isAddressListed(address(0x0))); + } - // Assert - Secondary - resBool = ruleWhitelist.isAddressListed(ADDRESS1); - assertEq(resBool, true); - resBool = ruleWhitelist.isAddressListed(ADDRESS2); - assertEq(resBool, true); - resUint256 = ruleWhitelist.listedAddressCount(); - assertEq(resUint256, 3); + /// @notice A DUPLICATE is still skipped (idempotent, and the event describes it truthfully) — + /// only the sentinel is rejected. + function testAddAddressesStillSkipsDuplicates() public { + assertEq(ruleWhitelist.listedAddressCount(), 0); + address[] memory whitelist = new address[](3); + whitelist[0] = ADDRESS1; + whitelist[1] = ADDRESS2; + whitelist[2] = ADDRESS1; // duplicate + + vm.prank(WHITELIST_OPERATOR_ADDRESS); + ruleWhitelist.addAddresses(whitelist); + + assertTrue(ruleWhitelist.isAddressListed(ADDRESS1)); + assertTrue(ruleWhitelist.isAddressListed(ADDRESS2)); + assertEq(ruleWhitelist.listedAddressCount(), 2); } function testAddAddressTwiceToTheWhitelist() public { @@ -166,6 +171,8 @@ contract RuleWhitelistAddTest is Test, HelperContract { } function testFuzz_AddRemoveIdempotent(address addressA, address addressB) public { + // The zero address is the mint/burn sentinel and can never be listed. + vm.assume(addressA != address(0) && addressB != address(0)); address[] memory targets = new address[](3); targets[0] = addressA; targets[1] = addressB; diff --git a/test/RuleWhitelist/RuleWhitelistWrapperRuleEngineIntegration.t.sol b/test/RuleWhitelist/RuleWhitelistWrapperRuleEngineIntegration.t.sol index 555c609..33825b2 100644 --- a/test/RuleWhitelist/RuleWhitelistWrapperRuleEngineIntegration.t.sol +++ b/test/RuleWhitelist/RuleWhitelistWrapperRuleEngineIntegration.t.sol @@ -17,7 +17,7 @@ contract RuleWhitelistWrapperRuleEngineIntegration is Test, HelperContract { vm.prank(DEFAULT_ADMIN_ADDRESS); ruleWhitelist = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, false); vm.prank(DEFAULT_ADMIN_ADDRESS); - ruleWhitelistWrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true); + ruleWhitelistWrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, true); vm.prank(DEFAULT_ADMIN_ADDRESS); ruleWhitelistWrapper.addRule(ruleWhitelist); diff --git a/test/RuleWhitelist/WhitelistWrapper.t.sol b/test/RuleWhitelist/WhitelistWrapper.t.sol index 0a668f9..e2b3b60 100644 --- a/test/RuleWhitelist/WhitelistWrapper.t.sol +++ b/test/RuleWhitelist/WhitelistWrapper.t.sol @@ -27,7 +27,7 @@ contract CMTATIntegrationWhitelistWrapper is Test, HelperContract { ruleWhitelist = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true, false); ruleWhitelist2 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true, false); ruleWhitelist3 = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true, false); - ruleWhitelistWrapper = new RuleWhitelistWrapper(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true); + ruleWhitelistWrapper = new RuleWhitelistWrapper(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true, true); vm.prank(WHITELIST_OPERATOR_ADDRESS); ruleWhitelistWrapper.addRule(ruleWhitelist); vm.prank(WHITELIST_OPERATOR_ADDRESS); @@ -42,7 +42,7 @@ contract CMTATIntegrationWhitelistWrapper is Test, HelperContract { function testCannotDeployContractIfAdminAddressIsZero() public { vm.prank(WHITELIST_OPERATOR_ADDRESS); vm.expectRevert(AccessControlModuleStandalone.AccessControlModuleStandalone_AddressZeroNotAllowed.selector); - ruleWhitelistWrapper = new RuleWhitelistWrapper(ZERO_ADDRESS, ZERO_ADDRESS, true); + ruleWhitelistWrapper = new RuleWhitelistWrapper(ZERO_ADDRESS, ZERO_ADDRESS, true, true); } function testReturnTheRightMessageForAGivenCode() public { @@ -67,7 +67,8 @@ contract CMTATIntegrationWhitelistWrapper is Test, HelperContract { } function testWrapperWithZeroRulesRejectsTransfers() public { - RuleWhitelistWrapper emptyWrapper = new RuleWhitelistWrapper(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true); + RuleWhitelistWrapper emptyWrapper = + new RuleWhitelistWrapper(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true, true); resUint8 = emptyWrapper.detectTransferRestriction(ADDRESS1, ADDRESS2, 20); assertEq(resUint8, CODE_ADDRESS_FROM_NOT_WHITELISTED); @@ -335,13 +336,14 @@ contract CMTATIntegrationWhitelistWrapper is Test, HelperContract { } function testIsVerifiedWithNoChildRules() public { - RuleWhitelistWrapper emptyWrapper = new RuleWhitelistWrapper(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true); + RuleWhitelistWrapper emptyWrapper = + new RuleWhitelistWrapper(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true, true); assertFalse(emptyWrapper.isVerified(ADDRESS1)); } function testInternalTransferredSpenderOverload() public { RuleWhitelistWrapperHarnessInternal wrapperHarness = - new RuleWhitelistWrapperHarnessInternal(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true); + new RuleWhitelistWrapperHarnessInternal(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true, true); RuleWhitelist child = new RuleWhitelist(WHITELIST_OPERATOR_ADDRESS, ZERO_ADDRESS, true, false); vm.prank(WHITELIST_OPERATOR_ADDRESS); diff --git a/test/StaleState/StaleStateHygiene.t.sol b/test/StaleState/StaleStateHygiene.t.sol new file mode 100644 index 0000000..6fe5263 --- /dev/null +++ b/test/StaleState/StaleStateHygiene.t.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {RuleConditionalTransferLight} from "src/rules/operation/RuleConditionalTransferLight.sol"; +import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol"; +import {RuleMintAllowance} from "src/rules/operation/RuleMintAllowance.sol"; + +/** + * @title StaleStateHygiene + * @notice Covers the state-clearing functions added by improvement I-6 (threat `BIND-1`, finding F-9). + * @dev `unbindToken` does not clear `approvalCounts` / `mintAllowance`. These tests verify the new + * `resetApproval` / `clearMintAllowances` operations let an operator discard that state before + * rebinding, and that they are correctly access-controlled. + */ +contract StaleStateHygiene is Test, HelperContract { + /** + * @dev Redeclared locally: `HelperContract` already inherits the single-token invariant storage, + * whose constants clash with the multi-token variant's. + */ + error RuleConditionalTransferLightMultiToken_TransferApprovalNotFound(); + error RuleConditionalTransferLightMultiToken_InvalidToken(); + + address private constant MINTER = address(10); + + /*////////////////////////////////////////////////////////////// + RuleConditionalTransferLight.resetApproval + //////////////////////////////////////////////////////////////*/ + + /** + * @notice BIND-1: an operator can discard stale approvals before rebinding, so they are NOT + * consumable by the newly bound token. + */ + function test_ResetApprovalPreventsStaleApprovalSurvivingRebind() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.bindToken(ADDRESS1); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.approveTransfer(ADDRESS2, ADDRESS3, 10); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.unbindToken(ADDRESS1); + + // Cleanup must work while NOTHING is bound — that is the whole point. + vm.prank(DEFAULT_ADMIN_ADDRESS); + uint256 cleared = ruleConditionalTransferLight.resetApproval(ADDRESS2, ADDRESS3, 10); + assertEq(cleared, 1); + assertEq(ruleConditionalTransferLight.approvedCount(ADDRESS2, ADDRESS3, 10), 0); + + // The new token cannot consume the discarded approval. + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.bindToken(ATTACKER); + vm.prank(ATTACKER); + vm.expectRevert(TransferNotApproved.selector); + ruleConditionalTransferLight.transferred(ADDRESS2, ADDRESS3, 10); + } + + /** + * @notice `resetApproval` discards ALL outstanding approvals for the tuple in one call, + * unlike `cancelTransferApproval` which removes exactly one. + */ + function test_ResetApprovalClearsEveryOutstandingApproval() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.approveTransfer(ADDRESS2, ADDRESS3, 10); + ruleConditionalTransferLight.approveTransfer(ADDRESS2, ADDRESS3, 10); + ruleConditionalTransferLight.approveTransfer(ADDRESS2, ADDRESS3, 10); + assertEq(ruleConditionalTransferLight.approvedCount(ADDRESS2, ADDRESS3, 10), 3); + + vm.expectEmit(true, true, false, true); + emit TransferApprovalReset(ADDRESS2, ADDRESS3, 10, 3); + uint256 cleared = ruleConditionalTransferLight.resetApproval(ADDRESS2, ADDRESS3, 10); + vm.stopPrank(); + + assertEq(cleared, 3); + assertEq(ruleConditionalTransferLight.approvedCount(ADDRESS2, ADDRESS3, 10), 0); + } + + /** + * @notice Single-item convention: resetting a non-existent approval reverts. + */ + function test_ResetApprovalRevertsWhenNothingToClear() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectRevert(TransferApprovalNotFound.selector); + ruleConditionalTransferLight.resetApproval(ADDRESS2, ADDRESS3, 10); + } + + /** + * @notice `resetApproval` is restricted to the transfer approver. + */ + function test_ResetApprovalUnauthorizedReverts() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.approveTransfer(ADDRESS2, ADDRESS3, 10); + + vm.prank(ATTACKER); + vm.expectRevert(); + ruleConditionalTransferLight.resetApproval(ADDRESS2, ADDRESS3, 10); + + assertEq(ruleConditionalTransferLight.approvedCount(ADDRESS2, ADDRESS3, 10), 1); + } + + /*////////////////////////////////////////////////////////////// + RuleConditionalTransferLightMultiToken.resetApproval + //////////////////////////////////////////////////////////////*/ + + /** + * @notice The multi-token reset works on an UNBOUND token — `approveTransfer` requires the token + * to be bound, so without this the stranded approvals of F-4 would be unclearable. + */ + function test_MultiTokenResetApprovalWorksOnUnboundToken() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleConditionalTransferLightMultiToken rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); + rule.approveTransfer(ADDRESS1, ADDRESS2, ADDRESS3, 10); + assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, ADDRESS3, 10), 1); + + rule.unbindToken(ADDRESS1); + assertEq(rule.isTokenBound(ADDRESS1), false); + + // Re-approving is impossible once unbound... + vm.expectRevert(RuleConditionalTransferLightMultiToken_InvalidToken.selector); + rule.approveTransfer(ADDRESS1, ADDRESS2, ADDRESS3, 10); + + // ...but cleanup still works, which is exactly what it is for. + uint256 cleared = rule.resetApproval(ADDRESS1, ADDRESS2, ADDRESS3, 10); + vm.stopPrank(); + + assertEq(cleared, 1); + assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, ADDRESS3, 10), 0); + } + + /** + * @notice Multi-token reset reverts when there is nothing to clear, and is approver-gated. + */ + function test_MultiTokenResetApprovalGuards() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleConditionalTransferLightMultiToken rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); + + vm.expectRevert(RuleConditionalTransferLightMultiToken_TransferApprovalNotFound.selector); + rule.resetApproval(ADDRESS1, ADDRESS2, ADDRESS3, 10); + + rule.approveTransfer(ADDRESS1, ADDRESS2, ADDRESS3, 10); + vm.stopPrank(); + + vm.prank(ATTACKER); + vm.expectRevert(); + rule.resetApproval(ADDRESS1, ADDRESS2, ADDRESS3, 10); + assertEq(rule.approvedCount(ADDRESS1, ADDRESS2, ADDRESS3, 10), 1); + } + + /*////////////////////////////////////////////////////////////// + RuleMintAllowance.clearMintAllowances + //////////////////////////////////////////////////////////////*/ + + /** + * @notice BIND-1: an operator can discard stale quotas before rebinding, so they are NOT + * spendable through the newly bound token. + */ + function test_ClearMintAllowancesPreventsStaleQuotaSurvivingRebind() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleMintAllowance rule = new RuleMintAllowance(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); + rule.setMintAllowance(MINTER, 1000); + rule.unbindToken(ADDRESS1); + + // Cleanup must work while NOTHING is bound. + address[] memory minters = new address[](1); + minters[0] = MINTER; + rule.clearMintAllowances(minters); + assertEq(rule.mintAllowance(MINTER), 0); + + rule.bindToken(ATTACKER); + vm.stopPrank(); + + // The new binder cannot spend the discarded quota. + vm.prank(ATTACKER); + vm.expectRevert( + abi.encodeWithSelector(RuleMintAllowance_AllowanceExceeded.selector, address(rule), MINTER, 0, 400) + ); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 400); + } + + /** + * @notice Batch convention: clearing is idempotent and does not revert on already-zero minters. + */ + function test_ClearMintAllowancesIsIdempotentBatch() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleMintAllowance rule = new RuleMintAllowance(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(ADDRESS1, 500); + + address[] memory minters = new address[](3); + minters[0] = ADDRESS1; // has a quota + minters[1] = ADDRESS2; // already zero + minters[2] = ADDRESS1; // duplicate + + rule.clearMintAllowances(minters); + rule.clearMintAllowances(minters); // twice — still no revert + vm.stopPrank(); + + assertEq(rule.mintAllowance(ADDRESS1), 0); + assertEq(rule.mintAllowance(ADDRESS2), 0); + } + + /** + * @notice `clearMintAllowances` is restricted to the allowance operator. + */ + function test_ClearMintAllowancesUnauthorizedReverts() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleMintAllowance rule = new RuleMintAllowance(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + vm.stopPrank(); + + address[] memory minters = new address[](1); + minters[0] = MINTER; + + vm.prank(ATTACKER); + vm.expectRevert(); + rule.clearMintAllowances(minters); + + assertEq(rule.mintAllowance(MINTER), 1000); + } +} diff --git a/test/ThreatModel/ThreatModelTests.t.sol b/test/ThreatModel/ThreatModelTests.t.sol new file mode 100644 index 0000000..d692966 --- /dev/null +++ b/test/ThreatModel/ThreatModelTests.t.sol @@ -0,0 +1,631 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {CMTATDeployment} from "test/utils/CMTATDeployment.sol"; + +import {RuleEngine} from "RuleEngine/deployment/RuleEngine.sol"; +import {IRule} from "RuleEngine/interfaces/IRule.sol"; + +import {IdentityRegistryMock} from "src/mocks/IdentityRegistryMock.sol"; +import {TotalSupplyMock} from "src/mocks/TotalSupplyMock.sol"; +import {MockERC20WithTransferContext} from "src/mocks/MockERC20WithTransferContext.sol"; + +import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; +import {RuleWhitelistWrapper} from "src/rules/validation/deployment/RuleWhitelistWrapper.sol"; +import {RuleMaxTotalSupply} from "src/rules/validation/deployment/RuleMaxTotalSupply.sol"; +import {RuleIdentityRegistry} from "src/rules/validation/deployment/RuleIdentityRegistry.sol"; +import {RuleConditionalTransferLight} from "src/rules/operation/RuleConditionalTransferLight.sol"; +import {RuleConditionalTransferLightMultiToken} from "src/rules/operation/RuleConditionalTransferLightMultiToken.sol"; +import {RuleMintAllowance} from "src/rules/operation/RuleMintAllowance.sol"; + +/** + * @title ThreatModelTests + * @notice Proof-of-concept tests backing the findings recorded in THREAT_MODEL.md / RESULT.md. + * @dev Each test names the threat ID it exercises. Tests that assert a *current, undesirable* + * behaviour are named `*_CurrentBehaviour` so a future fix flags them for update. + */ +contract ThreatModelTests is Test, HelperContract { + /** + * @dev Redeclared locally: `HelperContract` already inherits the single-token + * `RuleConditionalTransferLightInvariantStorage`, whose constants clash with the + * multi-token variant's. + */ + error RuleConditionalTransferLightMultiToken_TransferNotApproved(); + + address private constant MINTER = address(10); + address private constant FORWARDER = address(0); + + /*////////////////////////////////////////////////////////////// + IR-1 — RuleIdentityRegistry is ERC-3643 conformant [FIXED — I-1] + //////////////////////////////////////////////////////////////*/ + + /** + * @notice IR-1 (regression): ERC-3643 states that `mint` "only require[s] the receiver to be + * whitelisted and verified on the Identity Registry". The minter is NOT screened, so a + * mint to a verified recipient succeeds even when the minter's own EOA is unregistered. + * This previously reverted with CODE_ADDRESS_SPENDER_NOT_VERIFIED (finding F-1). + */ + function test_IR1_MintSucceedsWhenMinterNotVerified() public { + IdentityRegistryMock registry = new IdentityRegistryMock(); + cmtatDeployment = new CMTATDeployment(); + cmtatContract = cmtatDeployment.cmtat(); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, address(cmtatContract)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleIdentityRegistry = new RuleIdentityRegistry(DEFAULT_ADMIN_ADDRESS, address(registry), false, false); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock.addRule(ruleIdentityRegistry); + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.setRuleEngine(ruleEngineMock); + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.grantRole(keccak256("MINTER_ROLE"), MINTER); + + // The recipient is KYC-verified. The minter (the issuer's operational EOA) is NOT. + registry.setVerified(ADDRESS1, true); + + vm.prank(MINTER); + cmtatContract.mint(ADDRESS1, 100); + assertEq(cmtatContract.balanceOf(ADDRESS1), 100); + } + + /** + * @notice IR-1 (regression): the receiver check IS mandated — a mint to an unverified recipient + * is still rejected. + */ + function test_IR1_MintStillRejectedWhenRecipientNotVerified() public { + IdentityRegistryMock registry = new IdentityRegistryMock(); + cmtatDeployment = new CMTATDeployment(); + cmtatContract = cmtatDeployment.cmtat(); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, address(cmtatContract)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleIdentityRegistry = new RuleIdentityRegistry(DEFAULT_ADMIN_ADDRESS, address(registry), false, false); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock.addRule(ruleIdentityRegistry); + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.setRuleEngine(ruleEngineMock); + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.grantRole(keccak256("MINTER_ROLE"), MINTER); + + registry.setVerified(MINTER, true); // minter verified, RECIPIENT is not + + vm.prank(MINTER); + vm.expectRevert(); + cmtatContract.mint(ADDRESS1, 100); + assertEq(cmtatContract.balanceOf(ADDRESS1), 0); + } + + /** + * @notice IR-1 (regression): the sender is not screened by default, so a DE-LISTED HOLDER can + * still exit their position to a verified counterparty — the reason ERC-3643 checks only + * the receiver. Previously the holder was trapped. + */ + function test_IR1_DelistedHolderCanStillExit() public { + IdentityRegistryMock registry = new IdentityRegistryMock(); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleIdentityRegistry = new RuleIdentityRegistry(DEFAULT_ADMIN_ADDRESS, address(registry), false, false); + + registry.setVerified(ADDRESS1, true); + registry.setVerified(ADDRESS2, true); + assertEq(ruleIdentityRegistry.detectTransferRestriction(ADDRESS1, ADDRESS2, 10), TRANSFER_OK); + + // ADDRESS1's identity lapses (claim expired / identity revoked). + registry.setVerified(ADDRESS1, false); + + // They can no longer RECEIVE... + assertEq(ruleIdentityRegistry.detectTransferRestriction(ADDRESS2, ADDRESS1, 10), CODE_ADDRESS_TO_NOT_VERIFIED); + // ...but they can still SEND to a verified counterparty, i.e. exit their position. + assertEq(ruleIdentityRegistry.detectTransferRestriction(ADDRESS1, ADDRESS2, 10), TRANSFER_OK); + } + + /** + * @notice IR-1 (contrast, unchanged): RuleWhitelist with `checkSpender = true` also exempts mints + * from the spender check. The two rules now agree. + */ + function test_IR1_WhitelistExemptsMintFromSpenderCheck() public { + cmtatDeployment = new CMTATDeployment(); + cmtatContract = cmtatDeployment.cmtat(); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, address(cmtatContract)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleWhitelist = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, true, true); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleWhitelist.addAddress(ADDRESS1); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock.addRule(ruleWhitelist); + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.setRuleEngine(ruleEngineMock); + vm.prank(DEFAULT_ADMIN_ADDRESS); + cmtatContract.grantRole(keccak256("MINTER_ROLE"), MINTER); + + assertEq(ruleWhitelist.isAddressListed(MINTER), false); + vm.prank(MINTER); + cmtatContract.mint(ADDRESS1, 100); + assertEq(cmtatContract.balanceOf(ADDRESS1), 100); + } + + /*////////////////////////////////////////////////////////////// + MTS-1 — RuleMaxTotalSupply views return a code (never revert) on overflow [FIXED — I-3] + //////////////////////////////////////////////////////////////*/ + + /** + * @notice MTS-1 (regression): `detectTransferRestriction`, `canTransfer` and + * `detectTransferRestrictionFrom` are ERC-1404 / ERC-3643 views that must never revert. + * After the overflow-safe fix (I-3), a `value` that would overflow `currentSupply + value` + * returns CODE_MAX_TOTAL_SUPPLY_EXCEEDED (resp. `false`) instead of panicking. + */ + function test_MTS1_DetectTransferRestrictionReturnsCodeOnOverflow() public { + TotalSupplyMock token = new TotalSupplyMock(); + token.setTotalSupply(1); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleMaxTotalSupply = new RuleMaxTotalSupply(DEFAULT_ADMIN_ADDRESS, address(token), 1000); + + assertEq( + ruleMaxTotalSupply.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, type(uint256).max), + CODE_MAX_TOTAL_SUPPLY_EXCEEDED + ); + assertEq(ruleMaxTotalSupply.canTransfer(ZERO_ADDRESS, ADDRESS1, type(uint256).max), false); + assertEq( + ruleMaxTotalSupply.detectTransferRestrictionFrom(ADDRESS3, ZERO_ADDRESS, ADDRESS1, type(uint256).max), + CODE_MAX_TOTAL_SUPPLY_EXCEEDED + ); + } + + /** + * @notice MTS-1 (regression): the overflow-safe view returns a code through a RuleEngine too, + * so the token-level ERC-1404 view of any CMTAT wired to this rule no longer reverts. + */ + function test_MTS1_OverflowReturnsCodeThroughRuleEngine() public { + TotalSupplyMock token = new TotalSupplyMock(); + token.setTotalSupply(1); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleMaxTotalSupply = new RuleMaxTotalSupply(DEFAULT_ADMIN_ADDRESS, address(token), 1000); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock.addRule(ruleMaxTotalSupply); + + assertEq( + ruleEngineMock.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, type(uint256).max), + CODE_MAX_TOTAL_SUPPLY_EXCEEDED + ); + } + + /** + * @notice MTS-1 (regression): fuzz the full uint256 domain, including the overflow region. + * The view must return the correct code and never revert. + */ + function testFuzz_MTS1_DetectRestrictionNeverReverts(uint256 currentSupply, uint256 value, uint256 maxSupply) + public + { + TotalSupplyMock token = new TotalSupplyMock(); + token.setTotalSupply(currentSupply); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleMaxTotalSupply = new RuleMaxTotalSupply(DEFAULT_ADMIN_ADDRESS, address(token), maxSupply); + + uint8 code = ruleMaxTotalSupply.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, value); + bool exceeds = currentSupply > maxSupply || value > maxSupply - currentSupply; + assertEq(code, exceeds ? CODE_MAX_TOTAL_SUPPLY_EXCEEDED : TRANSFER_OK); + } + + /** + * @notice MTS-1 (kept): the non-overflowing region still returns the correct code. + */ + function testFuzz_MTS1_DetectRestrictionBelowOverflow(uint256 currentSupply, uint256 value) public { + currentSupply = bound(currentSupply, 0, type(uint128).max); + value = bound(value, 0, type(uint256).max - currentSupply); + + TotalSupplyMock token = new TotalSupplyMock(); + token.setTotalSupply(currentSupply); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleMaxTotalSupply = new RuleMaxTotalSupply(DEFAULT_ADMIN_ADDRESS, address(token), type(uint128).max); + + uint8 code = ruleMaxTotalSupply.detectTransferRestriction(ZERO_ADDRESS, ADDRESS1, value); + bool exceeds = currentSupply + value > uint256(type(uint128).max); + assertEq(code, exceeds ? CODE_MAX_TOTAL_SUPPLY_EXCEEDED : TRANSFER_OK); + } + + /*////////////////////////////////////////////////////////////// + CTL-1 — approveAndTransferIfAllowed behind a RuleEngine [FIXED — I-5] + //////////////////////////////////////////////////////////////*/ + + /** + * @notice CTL-1 (regression): the helper WORKS behind a RuleEngine once the two binding roles are + * split — `bindToken(token)` for the ERC-20 target, `bindRuleEngine(engine)` for the + * authorized caller. It previously could not work at all: a single binding slot had to be + * both, and behind an engine those are different addresses (finding F-3). + * End-to-end coverage lives in + * `test/RuleConditionalTransferLight/RuleConditionalTransferLightBindRuleEngine.t.sol`. + */ + function test_CTL1_ApproveAndTransferIfAllowedWorksOnceEngineIsBound() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + + MockERC20WithTransferContext token = new MockERC20WithTransferContext("Token", "TKN"); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.bindToken(address(token)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.bindRuleEngine(address(ruleEngineMock)); + + // Both are authorized executors; the ERC-20 target is the TOKEN, never the engine. + assertTrue(ruleConditionalTransferLight.isTransferExecutor(address(token))); + assertTrue(ruleConditionalTransferLight.isTransferExecutor(address(ruleEngineMock))); + assertEq(ruleConditionalTransferLight.getTokenBound(), address(token)); + } + + /** + * @notice CTL-1 (regression): the two unsupported wirings still fail loudly. + * Binding ONLY the engine leaves `getTokenBound()` a non-ERC-20, so the helper reverts; + * binding ONLY the token leaves the engine unauthorized, so it cannot consume approvals. + */ + function test_CTL1_UnsupportedWiringsStillFail() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleEngineMock = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); + + // (a) engine bound as the "token": IERC20(engine).allowance(...) has no such function. + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleConditionalTransferLight engineOnly = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + engineOnly.bindToken(address(ruleEngineMock)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + vm.expectRevert(); + engineOnly.approveAndTransferIfAllowed(ADDRESS1, ADDRESS2, 10); + + // (b) token bound but engine NOT bound: the engine is not an authorized executor. + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleConditionalTransferLight tokenOnly = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + tokenOnly.bindToken(ADDRESS1); + assertFalse(tokenOnly.isTransferExecutor(address(ruleEngineMock))); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + tokenOnly.approveTransfer(ADDRESS2, ADDRESS3, 10); + vm.prank(address(ruleEngineMock)); + vm.expectRevert( + abi.encodeWithSelector( + RuleConditionalTransferLight_TransferExecutorUnauthorized.selector, address(ruleEngineMock) + ) + ); + tokenOnly.transferred(ADDRESS2, ADDRESS3, 10); + } + + /** + * @notice CTL-1 (documented caveat, NOT fixed): `RuleConditionalTransferLight` keys approvals on + * `(from, to, value)` with NO token dimension. A `RuleEngine` is multi-tenant, so if the + * bound engine serves several tokens they all share one approval bucket: an approval + * recorded for token A is consumable by a token-B transfer relayed through the same engine. + * + * This is inherent to the single-token rule (it is why the MultiToken variant exists) and + * is why `bindRuleEngine` documents "bind ONLY an engine that serves this one token". + * Pinned here so the constraint is a tested property rather than a doc claim. + */ + function test_CTL1_ApprovalsAreNotTokenScopedBehindAMultiTenantEngine_CurrentBehaviour() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleEngine engine = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); + RuleConditionalTransferLight rule = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); // token A + rule.bindRuleEngine(address(engine)); + rule.approveTransfer(ADDRESS2, ADDRESS3, 100); // intended for token A + vm.stopPrank(); + + assertEq(rule.approvedCount(ADDRESS2, ADDRESS3, 100), 1); + + // The engine relays a transfer of a DIFFERENT token it also serves. The rule cannot tell, + // and consumes the token-A approval. + vm.prank(address(engine)); + rule.transferred(ADDRESS2, ADDRESS3, 100); + + assertEq(rule.approvedCount(ADDRESS2, ADDRESS3, 100), 0); + } + + /** + * @notice CTL-1: only the bound entity may consume an approval. + */ + function test_CTL1_UnboundCallerCannotConsumeApproval() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.bindToken(ADDRESS1); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.approveTransfer(ADDRESS2, ADDRESS3, 10); + + vm.prank(ATTACKER); + vm.expectRevert(); + ruleConditionalTransferLight.transferred(ADDRESS2, ADDRESS3, 10); + + assertEq(ruleConditionalTransferLight.approvedCount(ADDRESS2, ADDRESS3, 10), 1); + } + + /*////////////////////////////////////////////////////////////// + CTL-2 — MultiToken approval-key mismatch under a RuleEngine + //////////////////////////////////////////////////////////////*/ + + /** + * @notice CTL-2: the multi-token rule stores approvals under the caller-supplied `token` + * but consumes them under `msg.sender`. Behind a RuleEngine those differ, so a + * token-keyed approval is never consumable and remains permanently stale. + */ + function test_CTL2_MultiTokenApprovalKeyMismatchLeavesStaleApproval_CurrentBehaviour() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleEngine engine = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleConditionalTransferLightMultiToken rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS); + + MockERC20WithTransferContext token = new MockERC20WithTransferContext("Token", "TKN"); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(address(engine)); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(address(token)); + + // Operator approves the transfer for the *token*, as the public API suggests. + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(address(token), ADDRESS2, ADDRESS3, 10); + assertEq(rule.approvedCount(address(token), ADDRESS2, ADDRESS3, 10), 1); + + // The RuleEngine is the actual caller of `transferred()`, so the key does not match. + vm.prank(address(engine)); + vm.expectRevert(RuleConditionalTransferLightMultiToken_TransferNotApproved.selector); + rule.transferred(ADDRESS2, ADDRESS2, ADDRESS3, 10); + + // The token-keyed approval is still there and can never be consumed in this topology. + assertEq(rule.approvedCount(address(token), ADDRESS2, ADDRESS3, 10), 1); + assertEq(rule.approvedCount(address(engine), ADDRESS2, ADDRESS3, 10), 0); + } + + /** + * @notice CTL-2: engine-keyed approvals ARE consumable, confirming the key really is + * `msg.sender`. In a RuleEngine bound to several tokens this collapses the + * per-token isolation the rule's name and documentation advertise. + */ + function test_CTL2_EngineKeyedApprovalIsSharedAcrossTokens_CurrentBehaviour() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleEngine engine = new RuleEngine(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleConditionalTransferLightMultiToken rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(address(engine)); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(address(engine), ADDRESS2, ADDRESS3, 10); + + // Any token routed through this engine consumes the same approval bucket. + vm.prank(address(engine)); + rule.transferred(ADDRESS2, ADDRESS2, ADDRESS3, 10); + assertEq(rule.approvedCount(address(engine), ADDRESS2, ADDRESS3, 10), 0); + } + + /** + * @notice CTL-2: `detectTransferRestriction` keys on `msg.sender`, so an off-chain + * integrator (or any caller that is not a bound token) always sees "not approved" + * even for an approved transfer. Fail-closed, but the ERC-1404 view carries no signal. + */ + function test_CTL2_DetectTransferRestrictionIsMsgSenderDependent_CurrentBehaviour() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleConditionalTransferLightMultiToken rule = new RuleConditionalTransferLightMultiToken(DEFAULT_ADMIN_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(ADDRESS1, ADDRESS2, ADDRESS3, 10); + + // Called by the bound token: approved. + vm.prank(ADDRESS1); + assertEq(rule.detectTransferRestriction(ADDRESS2, ADDRESS3, 10), TRANSFER_OK); + + // Called by anybody else (e.g. an off-chain `eth_call` from a wallet): not approved. + vm.prank(ATTACKER); + assertEq(rule.detectTransferRestriction(ADDRESS2, ADDRESS3, 10), CODE_TRANSFER_REQUEST_NOT_APPROVED); + } + + /*////////////////////////////////////////////////////////////// + BIND-1 — state survives unbind/rebind + //////////////////////////////////////////////////////////////*/ + + /** + * @notice BIND-1: `unbindToken` does not clear `approvalCounts`. An approval granted for + * token A survives a rebind to token B and is consumable by B. + */ + function test_BIND1_ConditionalTransferApprovalSurvivesRebind_CurrentBehaviour() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.bindToken(ADDRESS1); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.approveTransfer(ADDRESS2, ADDRESS3, 10); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.unbindToken(ADDRESS1); + vm.prank(DEFAULT_ADMIN_ADDRESS); + ruleConditionalTransferLight.bindToken(ATTACKER); + + assertEq(ruleConditionalTransferLight.approvedCount(ADDRESS2, ADDRESS3, 10), 1); + vm.prank(ATTACKER); + ruleConditionalTransferLight.transferred(ADDRESS2, ADDRESS3, 10); + assertEq(ruleConditionalTransferLight.approvedCount(ADDRESS2, ADDRESS3, 10), 0); + } + + /** + * @notice BIND-1: `unbindToken` does not clear `mintAllowance` either. + */ + function test_BIND1_MintAllowanceSurvivesRebind_CurrentBehaviour() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleMintAllowance rule = new RuleMintAllowance(DEFAULT_ADMIN_ADDRESS); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, 1000); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.unbindToken(ADDRESS1); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ATTACKER); + + assertEq(rule.mintAllowance(MINTER), 1000); + vm.prank(ATTACKER); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS1, 400); + assertEq(rule.mintAllowance(MINTER), 600); + } + + /*////////////////////////////////////////////////////////////// + MA-1 — RuleMintAllowance eligibility views carry no signal + //////////////////////////////////////////////////////////////*/ + + /** + * @notice MA-1: `canTransfer` / `detectTransferRestriction` are hardcoded to "allowed", + * so a pre-flight check disagrees with the enforcement path, which reverts. + */ + function test_MA1_HardcodedEligibilityViewsDisagreeWithEnforcement_CurrentBehaviour() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleMintAllowance rule = new RuleMintAllowance(DEFAULT_ADMIN_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); + + assertEq(rule.mintAllowance(MINTER), 0); + + // Pre-flight says "allowed" even though the minter has zero quota. + assertEq(rule.detectTransferRestriction(ZERO_ADDRESS, ADDRESS2, 100), TRANSFER_OK); + assertEq(rule.canTransfer(ZERO_ADDRESS, ADDRESS2, 100), true); + + // Only the spender-aware view carries the real answer. + assertEq(rule.canTransferFrom(MINTER, ZERO_ADDRESS, ADDRESS2, 100), false); + + // Enforcement reverts. + vm.prank(ADDRESS1); + vm.expectRevert(); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS2, 100); + } + + /** + * @notice MA-1: quota accounting never underflows and always matches consumed amounts. + */ + function testFuzz_MA1_MintAllowanceAccountingIsExact(uint128 quota, uint128 mintA, uint128 mintB) public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleMintAllowance rule = new RuleMintAllowance(DEFAULT_ADMIN_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.bindToken(ADDRESS1); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.setMintAllowance(MINTER, quota); + + uint256 consumed = 0; + uint128[2] memory mints = [mintA, mintB]; + for (uint256 i = 0; i < mints.length; ++i) { + vm.prank(ADDRESS1); + if (uint256(mints[i]) > uint256(quota) - consumed) { + vm.expectRevert(); + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS2, mints[i]); + } else { + rule.transferred(MINTER, ZERO_ADDRESS, ADDRESS2, mints[i]); + consumed += mints[i]; + } + } + assertEq(rule.mintAllowance(MINTER), uint256(quota) - consumed); + } + + /*////////////////////////////////////////////////////////////// + WW-1 / WW-2 — RuleWhitelistWrapper child-rule handling + //////////////////////////////////////////////////////////////*/ + + /** + * @notice WW-1: the wrapper's OR semantics span child rules per address, so a transfer is + * allowed when `from` is whitelisted in one child and `to` in a different child. + * No child rule on its own would allow it. + */ + function test_WW1_CrossRuleOrAllowsTransferNoSingleChildAllows() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelist childA = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false); + RuleWhitelist childB = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false); + childA.addAddress(ADDRESS1); + childB.addAddress(ADDRESS2); + + RuleWhitelistWrapper wrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + wrapper.addRule(IRule(address(childA))); + wrapper.addRule(IRule(address(childB))); + vm.stopPrank(); + + // Neither child alone permits ADDRESS1 -> ADDRESS2. + assertEq(childA.detectTransferRestriction(ADDRESS1, ADDRESS2, 10), CODE_ADDRESS_TO_NOT_WHITELISTED); + assertEq(childB.detectTransferRestriction(ADDRESS1, ADDRESS2, 10), CODE_ADDRESS_FROM_NOT_WHITELISTED); + + // The wrapper does. + assertEq(wrapper.detectTransferRestriction(ADDRESS1, ADDRESS2, 10), TRANSFER_OK); + assertEq(wrapper.canTransfer(ADDRESS1, ADDRESS2, 10), true); + } + + /** + * @notice WW-2: unlike `RuleEngineBase`, the wrapper does not ERC-165-check that a child + * rule implements `IAddressList`. Adding a conformant `IRule` that is not an + * address list bricks every transfer check that has to scan past the first child. + * Note the early-exit in `_detectTransferRestrictionForTargets`: a pair already + * resolved by an earlier child still succeeds, so the breakage is input-dependent. + */ + function test_WW2_NonAddressListChildRuleBricksWrapper_CurrentBehaviour() public { + TotalSupplyMock token = new TotalSupplyMock(); + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelist childA = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false); + childA.addAddress(ADDRESS1); + childA.addAddress(ADDRESS2); + + RuleWhitelistWrapper wrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + wrapper.addRule(IRule(address(childA))); + assertEq(wrapper.detectTransferRestriction(ADDRESS1, ADDRESS2, 10), TRANSFER_OK); + + // RuleMaxTotalSupply is a valid IRule but exposes no `areAddressesListed`. + RuleMaxTotalSupply notAnAddressList = new RuleMaxTotalSupply(DEFAULT_ADMIN_ADDRESS, address(token), 1000); + wrapper.addRule(IRule(address(notAnAddressList))); + vm.stopPrank(); + + // Both endpoints resolved by childA: the early-exit never reaches the broken child. + assertEq(wrapper.detectTransferRestriction(ADDRESS1, ADDRESS2, 10), TRANSFER_OK); + + // ADDRESS3 is listed nowhere, so the scan continues into the broken child and reverts + // instead of returning CODE_ADDRESS_TO_NOT_WHITELISTED. + vm.expectRevert(); + wrapper.detectTransferRestriction(ADDRESS1, ADDRESS3, 10); + } + + /** + * @notice WW-2: a wrapper with no child rules rejects every transfer (fail-closed). + */ + function test_WW2_EmptyWrapperFailsClosed() public { + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelistWrapper wrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, true); + assertEq(wrapper.detectTransferRestriction(ADDRESS1, ADDRESS2, 10), CODE_ADDRESS_FROM_NOT_WHITELISTED); + assertEq(wrapper.isVerified(ADDRESS1), false); + } + + /*////////////////////////////////////////////////////////////// + HASH-1 — approval hash injectivity + //////////////////////////////////////////////////////////////*/ + + /** + * @notice HASH-1: distinct `(from, to, value)` tuples must never share an approval bucket. + * Guards the hand-rolled `_transferHash` assembly against packing collisions. + */ + function testFuzz_HASH1_ApprovalBucketsAreDistinct(address from, address to, uint256 value, uint256 otherValue) + public + { + vm.assume(from != to); + vm.assume(value != otherValue); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleConditionalTransferLight rule = new RuleConditionalTransferLight(DEFAULT_ADMIN_ADDRESS); + vm.prank(DEFAULT_ADMIN_ADDRESS); + rule.approveTransfer(from, to, value); + + assertEq(rule.approvedCount(from, to, value), 1); + assertEq(rule.approvedCount(to, from, value), 0); + assertEq(rule.approvedCount(from, to, otherValue), 0); + } +} diff --git a/test/TransferContext/OverloadParity.t.sol b/test/TransferContext/OverloadParity.t.sol new file mode 100644 index 0000000..24e3327 --- /dev/null +++ b/test/TransferContext/OverloadParity.t.sol @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {HelperContract} from "../HelperContract.sol"; +import {ITransferContext} from "src/rules/interfaces/ITransferContext.sol"; +import {ISanctionsList} from "src/rules/interfaces/ISanctionsList.sol"; +import {IRule} from "RuleEngine/interfaces/IRule.sol"; + +import {IdentityRegistryMock} from "src/mocks/IdentityRegistryMock.sol"; +import {SanctionListOracle} from "src/mocks/SanctionListOracle.sol"; + +import {RuleWhitelist} from "src/rules/validation/deployment/RuleWhitelist.sol"; +import {RuleWhitelistWrapper} from "src/rules/validation/deployment/RuleWhitelistWrapper.sol"; +import {RuleBlacklist} from "src/rules/validation/deployment/RuleBlacklist.sol"; +import {RuleSpenderWhitelist} from "src/rules/validation/deployment/RuleSpenderWhitelist.sol"; +import {RuleSanctionsList} from "src/rules/validation/deployment/RuleSanctionsList.sol"; +import {RuleERC2980} from "src/rules/validation/deployment/RuleERC2980.sol"; +import {RuleIdentityRegistry} from "src/rules/validation/deployment/RuleIdentityRegistry.sol"; + +/** + * @notice Flattened view of every overload {RuleNFTAdapter} adds on top of {RuleTransferValidation}, + * so the parity assertions can be run generically across every rule that inherits it. + */ +interface INFTAdapterRule { + /* ==== read path — fungible (RuleTransferValidation) ==== */ + function detectTransferRestriction(address from, address to, uint256 value) external view returns (uint8); + function detectTransferRestrictionFrom(address spender, address from, address to, uint256 value) + external + view + returns (uint8); + function canTransfer(address from, address to, uint256 value) external view returns (bool); + function canTransferFrom(address spender, address from, address to, uint256 value) external view returns (bool); + + /* ==== read path — ERC-7943 tokenId overloads (RuleNFTAdapter) ==== */ + function detectTransferRestriction(address from, address to, uint256 tokenId, uint256 value) + external + view + returns (uint8); + function detectTransferRestrictionFrom(address spender, address from, address to, uint256 tokenId, uint256 value) + external + view + returns (uint8); + function canTransfer(address from, address to, uint256 tokenId, uint256 amount) external view returns (bool); + function canTransferFrom(address spender, address from, address to, uint256 tokenId, uint256 value) + external + view + returns (bool); + + /* ==== write path ==== */ + function transferred(address from, address to, uint256 value) external; + function transferred(address spender, address from, address to, uint256 value) external; + function transferred(address from, address to, uint256 tokenId, uint256 value) external; + function transferred(address spender, address from, address to, uint256 tokenId, uint256 value) external; + function transferred(ITransferContext.FungibleTransferContext calldata ctx) external; + function transferred(ITransferContext.MultiTokenTransferContext calldata ctx) external; +} + +/** + * @title OverloadParity + * @notice Improvement I-10d: exercises the ERC-7943 `tokenId` overloads and the {ITransferContext} + * struct entrypoints on EVERY rule that inherits {RuleNFTAdapter}, closing the residual + * coverage gap in `RuleNFTAdapter` / `RuleTransferValidation`. + * @dev The property under test is **parity**: `RuleNFTAdapter` exists only to re-expose the same + * restriction logic under extra signatures, ignoring `tokenId`. So for every rule and every + * input, the `tokenId` overload MUST be indistinguishable from its fungible counterpart, and + * the `ctx` entrypoints MUST dispatch to the same internal hooks. Any divergence is a bug. + * + * This also pins threat `AC-5`: the `ctx` entrypoints are `external` with no access control on + * validation rules. That is acceptable precisely because they are view-only — an unprivileged + * caller can run the check and maybe revert, but cannot mutate anything. + */ +contract OverloadParity is Test, HelperContract { + uint256 private constant TOKEN_ID = 42; + address private constant FORWARDER = address(0); + + IdentityRegistryMock private registry; + SanctionListOracle private oracle; + + function setUp() public { + registry = new IdentityRegistryMock(); + oracle = new SanctionListOracle(); + } + + /*////////////////////////////////////////////////////////////// + PARITY ASSERTIONS + //////////////////////////////////////////////////////////////*/ + + /// @dev The tokenId overloads must return exactly what the fungible ones return. + function _assertReadParity(address rule, address spender, address from, address to, uint256 value, string memory w) + internal + view + { + INFTAdapterRule r = INFTAdapterRule(rule); + + assertEq( + r.detectTransferRestriction(from, to, value), + r.detectTransferRestriction(from, to, TOKEN_ID, value), + string.concat(w, ": detectTransferRestriction") + ); + assertEq( + r.detectTransferRestrictionFrom(spender, from, to, value), + r.detectTransferRestrictionFrom(spender, from, to, TOKEN_ID, value), + string.concat(w, ": detectTransferRestrictionFrom") + ); + assertEq( + r.canTransfer(from, to, value), r.canTransfer(from, to, TOKEN_ID, value), string.concat(w, ": canTransfer") + ); + assertEq( + r.canTransferFrom(spender, from, to, value), + r.canTransferFrom(spender, from, to, TOKEN_ID, value), + string.concat(w, ": canTransferFrom") + ); + } + + /// @dev The write-path overloads must accept/reject identically to their fungible counterparts. + function _assertWriteParity( + address rule, + address spender, + address from, + address to, + uint256 value, + string memory w + ) internal { + bool direct = _try3(rule, from, to, value); + bool directNft = _try4Nft(rule, from, to, value); + assertEq(direct, directNft, string.concat(w, ": transferred(from,to,value) vs tokenId overload")); + + bool delegated = _try4From(rule, spender, from, to, value); + bool delegatedNft = _try5Nft(rule, spender, from, to, value); + assertEq(delegated, delegatedNft, string.concat(w, ": transferredFrom vs tokenId overload")); + + // ctx.sender == 0 dispatches to the direct hook... + assertEq( + _tryFungibleCtx(rule, ZERO_ADDRESS, from, to, value), + direct, + string.concat(w, ": FungibleContext(sender=0) must match transferred(from,to,value)") + ); + // ...and a spender that differs from `from` dispatches to the delegated hook. + assertEq( + _tryFungibleCtx(rule, spender, from, to, value), + delegated, + string.concat(w, ": FungibleContext(sender=spender) must match transferredFrom") + ); + assertEq( + _tryMultiCtx(rule, spender, from, to, value), + delegated, + string.concat(w, ": MultiTokenContext(sender=spender) must match transferredFrom") + ); + assertEq( + _tryMultiCtx(rule, ZERO_ADDRESS, from, to, value), + direct, + string.concat(w, ": MultiTokenContext(sender=0) must match transferred(from,to,value)") + ); + } + + /// @dev Runs both parity checks for an allowed pair and a blocked pair. + function _assertParity( + address rule, + address spender, + address okFrom, + address okTo, + address badFrom, + address badTo, + string memory what + ) internal { + _assertReadParity(rule, spender, okFrom, okTo, 10, string.concat(what, " [allowed]")); + _assertWriteParity(rule, spender, okFrom, okTo, 10, string.concat(what, " [allowed]")); + + _assertReadParity(rule, spender, badFrom, badTo, 10, string.concat(what, " [blocked]")); + _assertWriteParity(rule, spender, badFrom, badTo, 10, string.concat(what, " [blocked]")); + } + + /*////////////////////////////////////////////////////////////// + PER-RULE PARITY TESTS + //////////////////////////////////////////////////////////////*/ + + function test_Parity_RuleWhitelist() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelist rule = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, true, false); + rule.addAddress(ADDRESS1); + rule.addAddress(ADDRESS2); + rule.addAddress(ADDRESS3); + vm.stopPrank(); + + _assertParity(address(rule), ADDRESS3, ADDRESS1, ADDRESS2, ATTACKER, ADDRESS2, "RuleWhitelist"); + } + + function test_Parity_RuleWhitelistWrapper() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleWhitelist child = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER, false, false); + child.addAddress(ADDRESS1); + child.addAddress(ADDRESS2); + child.addAddress(ADDRESS3); + + RuleWhitelistWrapper wrapper = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, FORWARDER, true, true); + wrapper.addRule(IRule(address(child))); + vm.stopPrank(); + + _assertParity(address(wrapper), ADDRESS3, ADDRESS1, ADDRESS2, ATTACKER, ADDRESS2, "RuleWhitelistWrapper"); + } + + function test_Parity_RuleBlacklist() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleBlacklist rule = new RuleBlacklist(DEFAULT_ADMIN_ADDRESS, FORWARDER); + rule.addAddress(ATTACKER); + vm.stopPrank(); + + _assertParity(address(rule), ADDRESS3, ADDRESS1, ADDRESS2, ATTACKER, ADDRESS2, "RuleBlacklist"); + } + + function test_Parity_RuleSpenderWhitelist() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleSpenderWhitelist rule = new RuleSpenderWhitelist(DEFAULT_ADMIN_ADDRESS, FORWARDER); + rule.addAddress(ADDRESS3); + vm.stopPrank(); + + // Direct transfers are always allowed by this rule; only the spender leg can block. + _assertReadParity(address(rule), ADDRESS3, ADDRESS1, ADDRESS2, 10, "RuleSpenderWhitelist [ok spender]"); + _assertWriteParity(address(rule), ADDRESS3, ADDRESS1, ADDRESS2, 10, "RuleSpenderWhitelist [ok spender]"); + _assertReadParity(address(rule), ATTACKER, ADDRESS1, ADDRESS2, 10, "RuleSpenderWhitelist [bad spender]"); + _assertWriteParity(address(rule), ATTACKER, ADDRESS1, ADDRESS2, 10, "RuleSpenderWhitelist [bad spender]"); + } + + function test_Parity_RuleSanctionsList() public { + oracle.addToSanctionsList(ATTACKER); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleSanctionsList rule = + new RuleSanctionsList(DEFAULT_ADMIN_ADDRESS, FORWARDER, ISanctionsList(address(oracle))); + + _assertParity(address(rule), ADDRESS3, ADDRESS1, ADDRESS2, ATTACKER, ADDRESS2, "RuleSanctionsList"); + } + + function test_Parity_RuleERC2980() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleERC2980 rule = new RuleERC2980(DEFAULT_ADMIN_ADDRESS, FORWARDER, false); + rule.addWhitelistAddress(ADDRESS2); + rule.addFrozenlistAddress(ATTACKER); + vm.stopPrank(); + + _assertParity(address(rule), ADDRESS3, ADDRESS1, ADDRESS2, ATTACKER, ADDRESS2, "RuleERC2980"); + } + + function test_Parity_RuleIdentityRegistry() public { + registry.setVerified(ADDRESS1, true); + registry.setVerified(ADDRESS2, true); + registry.setVerified(ADDRESS3, true); + + vm.prank(DEFAULT_ADMIN_ADDRESS); + RuleIdentityRegistry rule = new RuleIdentityRegistry(DEFAULT_ADMIN_ADDRESS, address(registry), false, false); + + _assertParity(address(rule), ADDRESS3, ADDRESS1, ADDRESS2, ATTACKER, ADDRESS2, "RuleIdentityRegistry"); + } + + /*////////////////////////////////////////////////////////////// + AC-5 + //////////////////////////////////////////////////////////////*/ + + /** + * @notice AC-5: the `ctx` entrypoints on {RuleNFTAdapter} are `external` with NO access control. + * On a validation rule this is harmless because the hooks are view-only: an arbitrary + * caller can run the check (and be reverted by it) but can never mutate state. + */ + function test_AC5_ContextEntrypointsAreUnguardedButViewOnly() public { + vm.startPrank(DEFAULT_ADMIN_ADDRESS); + RuleBlacklist rule = new RuleBlacklist(DEFAULT_ADMIN_ADDRESS, FORWARDER); + rule.addAddress(ATTACKER); + vm.stopPrank(); + + // Anyone may call the ctx entrypoint for a permitted transfer; it simply succeeds. + vm.prank(ATTACKER); + assertTrue(_tryFungibleCtx(address(rule), ZERO_ADDRESS, ADDRESS1, ADDRESS2, 10), "unguarded call should pass"); + + // ...and it still enforces the rule for a blocked transfer. + vm.prank(ADDRESS1); + assertFalse( + _tryFungibleCtx(address(rule), ZERO_ADDRESS, ATTACKER, ADDRESS2, 10), "unguarded call must still enforce" + ); + + // No state was mutated: the blacklist is untouched. + assertTrue(rule.isAddressListed(ATTACKER)); + assertEq(rule.listedAddressCount(), 1); + } + + /*////////////////////////////////////////////////////////////// + LOW-LEVEL TRY HELPERS + //////////////////////////////////////////////////////////////*/ + + function _try3(address rule, address from, address to, uint256 value) internal returns (bool ok) { + try INFTAdapterRule(rule).transferred(from, to, value) { + ok = true; + } catch { + ok = false; + } + } + + function _try4From(address rule, address spender, address from, address to, uint256 value) + internal + returns (bool ok) + { + try INFTAdapterRule(rule).transferred(spender, from, to, value) { + ok = true; + } catch { + ok = false; + } + } + + function _try4Nft(address rule, address from, address to, uint256 value) internal returns (bool ok) { + try INFTAdapterRule(rule).transferred(from, to, TOKEN_ID, value) { + ok = true; + } catch { + ok = false; + } + } + + function _try5Nft(address rule, address spender, address from, address to, uint256 value) + internal + returns (bool ok) + { + try INFTAdapterRule(rule).transferred(spender, from, to, TOKEN_ID, value) { + ok = true; + } catch { + ok = false; + } + } + + function _tryFungibleCtx(address rule, address sender, address from, address to, uint256 value) + internal + returns (bool ok) + { + ITransferContext.FungibleTransferContext memory ctx = ITransferContext.FungibleTransferContext({ + selector: bytes4(0), sender: sender, from: from, to: to, value: value, data: "" + }); + try INFTAdapterRule(rule).transferred(ctx) { + ok = true; + } catch { + ok = false; + } + } + + function _tryMultiCtx(address rule, address sender, address from, address to, uint256 value) + internal + returns (bool ok) + { + ITransferContext.MultiTokenTransferContext memory ctx = ITransferContext.MultiTokenTransferContext({ + selector: bytes4(0), sender: sender, from: from, to: to, value: value, tokenId: TOKEN_ID, data: "" + }); + try INFTAdapterRule(rule).transferred(ctx) { + ok = true; + } catch { + ok = false; + } + } +} diff --git a/test/Version.t.sol b/test/Version.t.sol index 9bc8030..e810c4c 100644 --- a/test/Version.t.sol +++ b/test/Version.t.sol @@ -13,7 +13,7 @@ import {RuleERC2980} from "src/rules/validation/deployment/RuleERC2980.sol"; import {RuleConditionalTransferLight} from "src/rules/operation/RuleConditionalTransferLight.sol"; contract VersionTest is Test, HelperContract { - string constant EXPECTED_VERSION = "0.3.0"; + string constant EXPECTED_VERSION = "0.4.0"; function testVersionRuleWhitelist() public { RuleWhitelist rule = new RuleWhitelist(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, true, false); @@ -37,7 +37,7 @@ contract VersionTest is Test, HelperContract { } function testVersionRuleWhitelistWrapper() public { - RuleWhitelistWrapper rule = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, false); + RuleWhitelistWrapper rule = new RuleWhitelistWrapper(DEFAULT_ADMIN_ADDRESS, ZERO_ADDRESS, false, true); assertEq(rule.version(), EXPECTED_VERSION); } diff --git a/test/invariant/ConditionalTransferHandler.sol b/test/invariant/ConditionalTransferHandler.sol new file mode 100644 index 0000000..7ee1cb8 --- /dev/null +++ b/test/invariant/ConditionalTransferHandler.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {RuleConditionalTransferLight} from "src/rules/operation/RuleConditionalTransferLight.sol"; + +/** + * @title ConditionalTransferHandler + * @notice Invariant-test handler driving the approval state machine of {RuleConditionalTransferLight}. + * @dev The handler is itself the entity bound to the rule (`bindToken(handler)`), so it can call the + * `transferred` execution hooks directly, and it holds `OPERATOR_ROLE` so it can approve/cancel. + * + * Ghost counters track every accepted operation. The suite asserts: + * totalApproved - totalCancelled - totalExecuted == Σ approvalCounts + * i.e. approvals are conserved: each `approveTransfer` is eventually either cancelled, consumed, + * or still outstanding — never double-spent and never lost (INV-5). + * + * Calls that would revert are skipped rather than attempted, so the accounting reflects only + * operations the rule actually accepted. + */ +contract ConditionalTransferHandler is Test { + /** + * @notice The rule under test. + */ + RuleConditionalTransferLight public immutable rule; + + /** + * @notice Number of approvals successfully recorded. + */ + uint256 public totalApproved; + /** + * @notice Number of approvals successfully cancelled. + */ + uint256 public totalCancelled; + /** + * @notice Number of approvals successfully consumed by a transfer. + */ + uint256 public totalExecuted; + /** + * @notice Number of mint/burn callbacks made (these must never consume an approval). + */ + uint256 public mintBurnCalls; + + /** + * @notice A (from, to, value) tuple that has been approved at least once. + */ + struct TransferKey { + address from; + address to; + uint256 value; + } + + TransferKey[] internal _keys; + mapping(bytes32 hash => bool seen) internal _seen; + + /** + * @dev Small actor set and value range so the fuzzer collides on the same tuples often. + */ + address[3] internal _actors = [address(0xA1), address(0xA2), address(0xA3)]; + + constructor(RuleConditionalTransferLight rule_) { + rule = rule_; + } + + /*////////////////////////////////////////////////////////////// + HANDLER ACTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Records one approval for a fuzzed (from, to, value) tuple. + */ + function approve(uint256 fromSeed, uint256 toSeed, uint256 valueSeed) external { + (address from, address to, uint256 value) = _tuple(fromSeed, toSeed, valueSeed); + rule.approveTransfer(from, to, value); + totalApproved += 1; + _record(from, to, value); + } + + /** + * @notice Cancels one outstanding approval, if any exists for the selected tuple. + */ + function cancel(uint256 idx) external { + if (_keys.length == 0) { + return; + } + TransferKey memory k = _keys[idx % _keys.length]; + if (rule.approvedCount(k.from, k.to, k.value) == 0) { + return; + } + rule.cancelTransferApproval(k.from, k.to, k.value); + totalCancelled += 1; + } + + /** + * @notice Consumes one outstanding approval via the execution hook (handler is the bound entity). + */ + function execute(uint256 idx) external { + if (_keys.length == 0) { + return; + } + TransferKey memory k = _keys[idx % _keys.length]; + if (rule.approvedCount(k.from, k.to, k.value) == 0) { + return; + } + rule.transferred(k.from, k.to, k.value); + totalExecuted += 1; + } + + /** + * @notice Fires a mint or burn callback. These are exempt from approval consumption, so they must + * leave `approvalCounts` untouched — deliberately NOT counted in `totalExecuted`, which is + * what makes the conservation invariant prove the exemption. + */ + function executeMintOrBurn(uint256 seed, uint256 valueSeed) external { + address other = _actors[seed % _actors.length]; + uint256 value = bound(valueSeed, 0, 1e24); + if (seed % 2 == 0) { + rule.transferred(address(0), other, value); + } else { + rule.transferred(other, address(0), value); + } + mintBurnCalls += 1; + } + + /*////////////////////////////////////////////////////////////// + VIEWS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Sum of `approvalCounts` across every tuple ever approved. + */ + function sumApprovalCounts() external view returns (uint256 sum) { + for (uint256 i = 0; i < _keys.length; ++i) { + TransferKey memory k = _keys[i]; + sum += rule.approvedCount(k.from, k.to, k.value); + } + } + + /** + * @notice Number of distinct tuples approved at least once. + */ + function keyCount() external view returns (uint256) { + return _keys.length; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + + function _tuple(uint256 fromSeed, uint256 toSeed, uint256 valueSeed) + internal + view + virtual + returns (address from, address to, uint256 value) + { + from = _actors[fromSeed % _actors.length]; + to = _actors[toSeed % _actors.length]; + value = bound(valueSeed, 1, 5); + } + + function _record(address from, address to, uint256 value) internal virtual { + bytes32 key = keccak256(abi.encode(from, to, value)); + if (!_seen[key]) { + _seen[key] = true; + _keys.push(TransferKey(from, to, value)); + } + } +} diff --git a/test/invariant/MintAllowanceHandler.sol b/test/invariant/MintAllowanceHandler.sol new file mode 100644 index 0000000..1ecb665 --- /dev/null +++ b/test/invariant/MintAllowanceHandler.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {RuleMintAllowance} from "src/rules/operation/RuleMintAllowance.sol"; + +/** + * @title MintAllowanceHandler + * @notice Invariant-test handler driving the mint-quota accounting of {RuleMintAllowance}. + * @dev The handler is the entity bound to the rule (`bindToken(handler)`), so it can call the + * `transferred` execution hook directly, and it holds `ALLOWANCE_OPERATOR_ROLE` so it can + * set/increase/decrease quotas. + * + * It maintains a ghost mirror of the expected allowance per minter. The suite asserts: + * - `rule.mintAllowance(m) == ghostAllowance[m]` for every minter (exact accounting, INV-7) + * - `totalMinted <= totalCredited` (a minter can never mint more than was ever granted) + * + * Calls that would revert are skipped rather than attempted; if a rule call did revert, the whole + * handler call reverts and the ghost update rolls back with it, so the mirror stays consistent. + */ +contract MintAllowanceHandler is Test { + /** + * @notice The rule under test. + */ + RuleMintAllowance public immutable rule; + + /** + * @notice Sum of every amount ever credited (absolute sets + increases). + */ + uint256 public totalCredited; + /** + * @notice Sum of every amount ever minted through the quota. + */ + uint256 public totalMinted; + /** + * @notice Number of non-mint `transferred` calls made (these must never touch a quota). + */ + uint256 public regularTransferCalls; + + /** + * @notice Ghost mirror of the expected remaining allowance per minter. + */ + mapping(address minter => uint256 allowance) public ghostAllowance; + + /** + * @dev Small minter set so the fuzzer repeatedly hits the same quotas. + */ + address[3] internal _minters = [address(0xB1), address(0xB2), address(0xB3)]; + + /** + * @dev Upper bound per credit, keeping `totalCredited` far from overflow across all runs. + */ + uint256 internal constant MAX_CREDIT = 1e30; + + constructor(RuleMintAllowance rule_) { + rule = rule_; + } + + /*////////////////////////////////////////////////////////////// + HANDLER ACTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Sets a minter's quota to an absolute amount. + */ + function setAllowance(uint256 minterSeed, uint256 amount) external { + address minter = _minter(minterSeed); + amount = bound(amount, 0, MAX_CREDIT); + rule.setMintAllowance(minter, amount); + ghostAllowance[minter] = amount; + totalCredited += amount; + } + + /** + * @notice Increases a minter's quota. + */ + function increase(uint256 minterSeed, uint256 amount) external { + address minter = _minter(minterSeed); + amount = bound(amount, 0, MAX_CREDIT); + rule.increaseMintAllowance(minter, amount); + ghostAllowance[minter] += amount; + totalCredited += amount; + } + + /** + * @notice Decreases a minter's quota, staying within the current balance so the call is accepted. + */ + function decrease(uint256 minterSeed, uint256 amount) external { + address minter = _minter(minterSeed); + uint256 current = ghostAllowance[minter]; + if (current == 0) { + return; + } + amount = bound(amount, 0, current); + rule.decreaseMintAllowance(minter, amount); + ghostAllowance[minter] = current - amount; + } + + /** + * @notice Mints within the minter's quota via the spender-aware hook (`from == address(0)`). + */ + function mint(uint256 minterSeed, uint256 toSeed, uint256 value) external { + address minter = _minter(minterSeed); + uint256 current = ghostAllowance[minter]; + if (current == 0) { + return; + } + value = bound(value, 1, current); + address to = _minters[toSeed % _minters.length]; + + rule.transferred(minter, address(0), to, value); + + ghostAllowance[minter] = current - value; + totalMinted += value; + } + + /** + * @notice A non-mint transfer (`from != address(0)`) must never touch any quota. The ghost is + * deliberately left unchanged, so the mirror invariant proves the rule ignores it. + */ + function regularTransfer(uint256 minterSeed, uint256 value) external { + address spender = _minter(minterSeed); + value = bound(value, 0, 1e24); + rule.transferred(spender, _minters[0], _minters[1], value); + regularTransferCalls += 1; + } + + /*////////////////////////////////////////////////////////////// + VIEWS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the minter at `index` and its expected (ghost) allowance. + */ + function minterAt(uint256 index) external view returns (address minter, uint256 expected) { + minter = _minters[index % _minters.length]; + expected = ghostAllowance[minter]; + } + + /** + * @notice Number of distinct minters driven by the handler. + */ + function minterCount() external view returns (uint256) { + return _minters.length; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + + function _minter(uint256 seed) internal view virtual returns (address) { + return _minters[seed % _minters.length]; + } +} diff --git a/test/invariant/RuleInvariants.t.sol b/test/invariant/RuleInvariants.t.sol new file mode 100644 index 0000000..bebdff7 --- /dev/null +++ b/test/invariant/RuleInvariants.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {RuleConditionalTransferLight} from "src/rules/operation/RuleConditionalTransferLight.sol"; +import {RuleMintAllowance} from "src/rules/operation/RuleMintAllowance.sol"; +import {ConditionalTransferHandler} from "./ConditionalTransferHandler.sol"; +import {MintAllowanceHandler} from "./MintAllowanceHandler.sol"; + +/** + * @title ConditionalTransferInvariants + * @notice Stateful invariant suite over {RuleConditionalTransferLight}'s approval state machine. + * @dev Covers INV-5 (approvals are conserved and never underflow) — see TEST_IMPROVEMENT.md I-10b. + */ +contract ConditionalTransferInvariants is Test { + address private constant ADMIN = address(1); + + RuleConditionalTransferLight private rule; + ConditionalTransferHandler private handler; + + function setUp() public { + vm.startPrank(ADMIN); + rule = new RuleConditionalTransferLight(ADMIN); + handler = new ConditionalTransferHandler(rule); + + // The handler acts as the operator (approve/cancel) and as the bound entity (execute). + rule.grantRole(rule.OPERATOR_ROLE(), address(handler)); + rule.bindToken(address(handler)); + vm.stopPrank(); + + // Restrict fuzzing to the handler's own actions; without this the fuzzer would also call + // the public functions the handler inherits from forge-std's Test. + bytes4[] memory selectors = new bytes4[](4); + selectors[0] = ConditionalTransferHandler.approve.selector; + selectors[1] = ConditionalTransferHandler.cancel.selector; + selectors[2] = ConditionalTransferHandler.execute.selector; + selectors[3] = ConditionalTransferHandler.executeMintOrBurn.selector; + targetSelector(FuzzSelector({addr: address(handler), selectors: selectors})); + targetContract(address(handler)); + } + + /** + * @notice INV-5: every recorded approval is either still outstanding, cancelled, or consumed — + * exactly once. Approvals are never double-spent, never lost, and never underflow. + * + * totalApproved - totalCancelled - totalExecuted == Σ approvalCounts + * + * Because mint/burn callbacks are fired by the handler but deliberately not counted in + * `totalExecuted`, this equality also proves mint/burn never consume an approval. + */ + function invariant_approvalConservation() public view { + uint256 approved = handler.totalApproved(); + uint256 cancelled = handler.totalCancelled(); + uint256 executed = handler.totalExecuted(); + + assertGe(approved, cancelled + executed, "consumed more approvals than were ever recorded"); + assertEq(approved - cancelled - executed, handler.sumApprovalCounts(), "approval accounting drifted"); + } + + /** + * @notice INV-6 (corollary): an outstanding approval count can never exceed the number of + * approvals recorded for that tuple, so no tuple can be over-consumed. + */ + function invariant_noApprovalExceedsTotalRecorded() public view { + assertLe(handler.sumApprovalCounts(), handler.totalApproved(), "outstanding exceeds recorded"); + } +} + +/** + * @title MintAllowanceInvariants + * @notice Stateful invariant suite over {RuleMintAllowance}'s quota accounting. + * @dev Covers INV-7 (quota is exact, monotonically consumed, never underflows) — TEST_IMPROVEMENT.md I-10b. + */ +contract MintAllowanceInvariants is Test { + address private constant ADMIN = address(1); + + RuleMintAllowance private rule; + MintAllowanceHandler private handler; + + function setUp() public { + vm.startPrank(ADMIN); + rule = new RuleMintAllowance(ADMIN); + handler = new MintAllowanceHandler(rule); + + // The handler acts as the allowance operator and as the bound entity (mint callbacks). + rule.grantRole(rule.ALLOWANCE_OPERATOR_ROLE(), address(handler)); + rule.bindToken(address(handler)); + vm.stopPrank(); + + // Restrict fuzzing to the handler's own actions (see note in ConditionalTransferInvariants). + bytes4[] memory selectors = new bytes4[](5); + selectors[0] = MintAllowanceHandler.setAllowance.selector; + selectors[1] = MintAllowanceHandler.increase.selector; + selectors[2] = MintAllowanceHandler.decrease.selector; + selectors[3] = MintAllowanceHandler.mint.selector; + selectors[4] = MintAllowanceHandler.regularTransfer.selector; + targetSelector(FuzzSelector({addr: address(handler), selectors: selectors})); + targetContract(address(handler)); + } + + /** + * @notice INV-7: the on-chain quota exactly mirrors the expected value after any interleaving of + * set / increase / decrease / mint / regular-transfer. Because `regularTransfer` never + * updates the ghost, this equality also proves non-mint transfers do not touch the quota. + */ + function invariant_allowanceMatchesGhost() public view { + uint256 count = handler.minterCount(); + for (uint256 i = 0; i < count; ++i) { + (address minter, uint256 expected) = handler.minterAt(i); + assertEq(rule.mintAllowance(minter), expected, "mint allowance drifted from expected"); + } + } + + /** + * @notice INV-7: a minter can never mint more in total than was ever credited to it. + */ + function invariant_mintedNeverExceedsCredited() public view { + assertLe(handler.totalMinted(), handler.totalCredited(), "minted more than was ever granted"); + } +} diff --git a/test/utils/CMTATDeployment.sol b/test/utils/CMTATDeployment.sol new file mode 100644 index 0000000..0ce653f --- /dev/null +++ b/test/utils/CMTATDeployment.sol @@ -0,0 +1,32 @@ +//SPDX-License-Identifier: MPL-2.0 +pragma solidity ^0.8.20; + +import {ICMTATConstructor, CMTATStandardStandalone} from "CMTAT/deployment/CMTATStandardStandalone.sol"; +import {IRuleEngine} from "CMTAT/interfaces/engine/IRuleEngine.sol"; +import {IERC1643CMTAT} from "CMTAT/interfaces/tokenization/draft-IERC1643CMTAT.sol"; + +contract CMTATDeployment { + // Share with helper contract + address constant ZERO_ADDRESS = address(0); + address constant DEFAULT_ADMIN_ADDRESS = address(1); + + CMTATStandardStandalone public cmtat; + + constructor() { + // CMTAT + ICMTATConstructor.ERC20Attributes memory erc20Attributes = + ICMTATConstructor.ERC20Attributes("CMTA Token", "CMTAT", 0); + ICMTATConstructor.ExtraInformationAttributes memory extraInformationAttributes = + ICMTATConstructor.ExtraInformationAttributes( + "CMTAT_ISIN", + IERC1643CMTAT.DocumentInfo( + "Terms", "https://cmta.ch", 0x9ff867f6592aa9d6d039e7aad6bd71f1659720cbc4dd9eae1554f6eab490098b + ), + "CMTAT_info" + ); + ICMTATConstructor.Engine memory engines = ICMTATConstructor.Engine(IRuleEngine(ZERO_ADDRESS)); + cmtat = new CMTATStandardStandalone( + ZERO_ADDRESS, DEFAULT_ADMIN_ADDRESS, erc20Attributes, extraInformationAttributes, engines + ); + } +}