Skip to content

Conversation

james-toussaint
Copy link
Contributor

@james-toussaint james-toussaint commented Sep 5, 2025

Summary by Sourcery

Support pluggable compliance modules for confidential RWAs by defining new module interfaces, extending ERC7984Rwa to install and enforce identity and transfer compliance checks, and unifying the compliance hook naming

New Features:

  • Add interfaces for compliance modules including identity and transfer compliance
  • Implement ERC7984RwaCompliance extension to allow dynamic installation and enforcement of compliance modules on RWA transfers

Enhancements:

  • Rename compliance check hook from isCompliantTransfer to isCompliant across interfaces, core contract, and mock
  • Update ERC7984Rwa core logic to use the unified isCompliant hook

Chores:

  • Add base ERC7984RwaComplianceModule contract
  • Include changeset to record minor version change for compliance modules support

Summary by CodeRabbit

  • New Features

    • Added modular compliance for confidential RWAs with installable/uninstallable modules (e.g., always-on and transfer-only).
    • Introduced role visibility checks for admins and agents.
    • Added investor cap and balance cap compliance modules, including pre/post-transfer hooks.
    • Enhanced transfer flows to enforce compliance checks and emit relevant events.
  • Tests

    • Added comprehensive test suite covering module installation, role access, caps enforcement, encrypted transfers, and error scenarios.
  • Chores

    • Introduced a changeset for a minor version release.

@arr00
Copy link
Contributor

arr00 commented Sep 10, 2025

I would follow function naming from the existing ERC3643 module https://github.com/ERC-3643/ERC-3643/blob/main/contracts/compliance/modular/IModularCompliance.sol

Base automatically changed from feature/confidential-rwa to master September 29, 2025 07:59
Copy link

netlify bot commented Sep 30, 2025

Deploy Preview for confidential-tokens ready!

Name Link
🔨 Latest commit d3240e1
🔍 Latest deploy log https://app.netlify.com/projects/confidential-tokens/deploys/68dbddebd7d06f0008e45bad
😎 Deploy Preview https://deploy-preview-197--confidential-tokens.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@james-toussaint james-toussaint marked this pull request as ready for review September 30, 2025 13:56
@james-toussaint james-toussaint requested a review from a team as a code owner September 30, 2025 13:56
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (8)
contracts/mocks/token/ERC7984RwaComplianceModuleMock.sol (1)

18-24: Consider conventional naming for test helper functions.

The $_ prefix is unconventional in Solidity outside of Foundry's internal testing conventions. Consider using more standard names like setCompliant() and unsetCompliant() for clarity, unless this prefix is an established convention in your codebase.

contracts/token/ERC7984/extensions/rwa/ERC7984RwaInvestorCapModule.sol (2)

59-71: Consider adding admin access to investor count for monitoring.

The _postTransfer logic correctly maintains the investor count:

  • Increments when a recipient receives their first tokens (balance equals transferred amount)
  • Decrements when a sender's balance reaches zero

However, line 69 only calls FHE.allowThis(_investors), which allows the contract itself to read the encrypted count. Consider also granting access to token admins for monitoring purposes, similar to how the test suite expects admins to decrypt the current investor count (see test line 379).

Add after line 69:

 _investors = FHE.select(FHE.eq(balance, FHE.asEuint64(0)), FHE.sub(_investors, FHE.asEuint64(1)), _investors);
 FHE.allowThis(_investors);
+FHE.allow(_investors, _token);

This would allow the token contract (and by extension, its admins through getHandleAllowance) to decrypt the investor count.


64-70: Refine investor‐count logic

  • Ensure FHE lazy‐initialization here matches other modules so _investors is never used uninitialized.
  • Handle zero‐amount transfers explicitly or document that when encryptedAmount == 0, both selects fire but net effect is zero.
  • Prevent potential underflow in FHE.sub(_investors, FHE.asEuint64(1)) by guarding _investors > 0 or adding an assertion.
contracts/token/ERC7984/extensions/rwa/ERC7984RwaModularCompliance.sol (2)

35-35: Consider consistent error naming.

The error SenderNotComplianceModule doesn't follow the ERC7984Rwa prefix convention used by other errors in this contract (lines 27-33). For consistency, consider renaming to ERC7984RwaSenderNotComplianceModule.

Apply this diff:

-    error SenderNotComplianceModule(address account);
+    error ERC7984RwaSenderNotComplianceModule(address account);

And update line 211:

-            SenderNotComplianceModule(msg.sender)
+            ERC7984RwaSenderNotComplianceModule(msg.sender)

149-187: Clarify and enforce non-mutating compliance checks
The public isCompliantTransfer wrapper intentionally updates the FHE context via FHE.allow. To preserve the intended side-effect isolation, require that each module’s internal _isCompliantTransfer implementation be pure or view with no state mutations. Update the IERC7984RwaComplianceModule interface documentation accordingly and audit existing modules (e.g., BalanceCapModule, InvestorCapModule) to confirm compliance.

contracts/token/ERC7984/extensions/rwa/ERC7984RwaBalanceCapModule.sol (1)

6-6: Remove unused import.

The EnumerableSet import on line 6 is not used in this contract. Consider removing it to keep imports clean.

Apply this diff:

-import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

Also remove the using directive on line 15:

-    using EnumerableSet for *;
contracts/interfaces/IERC7984Rwa.sol (1)

86-94: Consider strengthening non-mutating guidance.

The interface correctly implements the pre-check (isCompliantTransfer) and post-hook (postTransfer) pattern from ERC-3643. However, the "should be non-mutating" comment on line 90 is advisory only. Module implementations could violate this intent, leading to unexpected state changes during compliance checks.

Consider adding this guidance to the interface documentation:

 /// @dev Interface for confidential RWA transfer compliance module.
+/// IMPORTANT: isCompliantTransfer should NOT mutate state. State mutations should be
+/// performed in postTransfer hooks. This separation ensures predictable compliance checks.
 interface IERC7984RwaComplianceModule {

Based on learnings from ERC-3643 and PR comments.

contracts/token/ERC7984/extensions/rwa/ERC7984RwaComplianceModule.sol (1)

48-50: Add zero address validation.

The constructor does not validate that token is not the zero address. This could lead to deployment issues that are difficult to debug.

Apply this diff to add validation:

 constructor(address token) {
+    require(token != address(0), "Token address cannot be zero");
     _token = token;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9d44c4 and d3240e1.

📒 Files selected for processing (12)
  • .changeset/wet-results-doubt.md (1 hunks)
  • contracts/interfaces/IERC7984Rwa.sol (2 hunks)
  • contracts/mocks/token/ERC7984RwaBalanceCapModuleMock.sol (1 hunks)
  • contracts/mocks/token/ERC7984RwaComplianceModuleMock.sol (1 hunks)
  • contracts/mocks/token/ERC7984RwaInvestorCapModuleMock.sol (1 hunks)
  • contracts/mocks/token/ERC7984RwaModularComplianceMock.sol (1 hunks)
  • contracts/token/ERC7984/extensions/ERC7984Rwa.sol (1 hunks)
  • contracts/token/ERC7984/extensions/rwa/ERC7984RwaBalanceCapModule.sol (1 hunks)
  • contracts/token/ERC7984/extensions/rwa/ERC7984RwaComplianceModule.sol (1 hunks)
  • contracts/token/ERC7984/extensions/rwa/ERC7984RwaInvestorCapModule.sol (1 hunks)
  • contracts/token/ERC7984/extensions/rwa/ERC7984RwaModularCompliance.sol (1 hunks)
  • test/token/ERC7984/extensions/ERC7984RwaModularCompliance.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
test/token/ERC7984/extensions/ERC7984RwaModularCompliance.test.ts (1)
test/helpers/event.ts (1)
  • callAndGetResult (5-9)
🔇 Additional comments (46)
.changeset/wet-results-doubt.md (1)

1-5: LGTM!

The changeset correctly documents the new modular compliance feature with an appropriate minor version bump.

contracts/token/ERC7984/extensions/ERC7984Rwa.sol (1)

69-72: LGTM!

The isAdminOrAgent function provides a clean convenience method for combined role checks, which will be useful for modular compliance module management. The implementation correctly delegates to existing role check functions and is marked virtual to allow overrides.

contracts/mocks/token/ERC7984RwaModularComplianceMock.sol (1)

1-17: LGTM!

The mock contract correctly combines ERC7984RwaModularCompliance with SepoliaConfig for testing. Constructor delegation to parent contracts is accurate and follows the established pattern used in other mocks in this PR.

contracts/mocks/token/ERC7984RwaInvestorCapModuleMock.sol (1)

1-10: LGTM!

The mock correctly inherits from ERC7984RwaInvestorCapModule and SepoliaConfig, with proper constructor delegation. The implementation is minimal and appropriate for testing purposes.

contracts/mocks/token/ERC7984RwaBalanceCapModuleMock.sol (1)

1-19: LGTM!

The mock extends ERC7984RwaBalanceCapModule appropriately for testing. The createEncryptedAmount helper function provides a useful test utility that:

  • Converts plaintext amounts to encrypted euint64 values
  • Properly authorizes the encrypted value for both the contract and the caller
  • Emits an event for test verification

The FHE authorization pattern is correct for test scenarios where both the contract and the caller need access to the encrypted value.

contracts/mocks/token/ERC7984RwaComplianceModuleMock.sol (3)

10-10: LGTM! Mock contract structure is appropriate for testing.

The contract correctly extends the base compliance module and provides a simple boolean flag to simulate compliance state for testing purposes.


13-16: LGTM! Events and constructor are correctly implemented.

The parameterless events are suitable for testing module hook invocation, and the constructor properly delegates to the base module.


26-37: LGTM! Mock correctly implements the two-function compliance pattern.

The implementation follows the ERC-3643-inspired pattern suggested in PR comments: a pre-transfer check (_isCompliantTransfer) and a post-transfer hook (_postTransfer). The event emissions enable test verification of module invocation.

test/token/ERC7984/extensions/ERC7984RwaModularCompliance.test.ts (5)

16-52: LGTM! Comprehensive fixture setup.

The fixture properly deploys the token, multiple compliance modules, and configures encrypted inputs. Good organization for testing various module types and scenarios.


54-115: LGTM! Thorough module lifecycle testing.

Excellent coverage of module support, installation, uninstallation, access control, and edge cases. The parameterized approach ensures both module types are tested consistently.


117-185: LGTM! Comprehensive compliance flow testing.

Excellent coverage of the core compliance mechanism with all combinations of force transfer and compliance states. The event assertions correctly verify that transferOnly modules are bypassed during force transfers while alwaysOn modules are always invoked.


187-358: LGTM! Comprehensive balance cap module testing.

Thorough coverage of the balance cap module including:

  • Both encrypted input paths (with and without proof)
  • Access control validation
  • Compliant and non-compliant transfer scenarios
  • Edge case handling (burning bypasses cap)

The test suite correctly verifies that transfers exceeding the balance cap result in zero transfer.


360-451: LGTM! Thorough investor cap module testing.

Comprehensive coverage of the investor cap module including:

  • Transfer compliance when cap is reached (correctly expects zero transfer)
  • Current investor tracking via encrypted counter
  • Admin functions and access control
  • Edge cases (transferring to existing investors)

Note: Fix the maxInverstor typo on lines 444 and 449 as mentioned in the earlier comment.

contracts/token/ERC7984/extensions/rwa/ERC7984RwaInvestorCapModule.sol (3)

1-20: LGTM! Well-structured investor cap module.

The contract structure is sound with appropriate state variables: plaintext max investor limit and encrypted current investor count. The constructor correctly initializes the base module and sets the initial cap.


22-36: LGTM! Public interface is well-designed.

The functions provide appropriate access:

  • Admin-only setter with event emission
  • Public getters for both max and current investor counts
  • Correctly returns encrypted current count to preserve privacy

38-57: Verify the compliance logic handles edge cases correctly.

The investor cap logic appears sound but should be verified:

  1. Burning check (line 49): Uses FHE.asEbool(to == address(0)) to check for zero address in plaintext. This is acceptable since the recipient address is not encrypted, but verify this pattern is consistent with your FHE design.

  2. Existing investor check (line 53): The condition FHE.gt(balance, FHE.asEuint64(0)) correctly identifies existing investors. However, consider the edge case where an investor's balance was reduced to zero in a previous transaction but they're still counted in _investors. The current logic would allow them to receive tokens again without incrementing the counter, which may be the intended behavior for "reactivating" an investor slot.

  3. Race condition consideration: In a concurrent environment, multiple transactions could potentially increment _investors simultaneously if they all pass the FHE.lt(_investors, FHE.asEuint64(_maxInvestor)) check before any completes. While FHE operations are atomic within a transaction, verify this behavior aligns with your threat model.

Consider documenting the intended behavior for these edge cases, particularly the "reactivation" scenario for investors whose balance returns from zero.

contracts/token/ERC7984/extensions/rwa/ERC7984RwaModularCompliance.sol (8)

1-16: LGTM!

The contract structure and imports are well-organized. The extension pattern appropriately builds on ERC7984Rwa, and the ERC-7579 inspiration is documented.


18-19: LGTM!

EnumerableSet is the right choice for tracking modules, enabling efficient iteration and membership checks.


37-52: LGTM!

The query functions provide clean public interfaces with appropriate visibility and extensibility.


54-66: LGTM!

The admin-only module management with gas footprint warning is well-designed. The delegation to internal functions enables safe extension.


78-94: LGTM!

The module validation using staticcall to isModule() and checking the returned magic value is a solid pattern for verifying module compatibility. The use of EnumerableSet's return values to detect duplicate installs is idiomatic.


111-147: LGTM! Design aligns with ERC-3643 pattern.

The pre-check and post-hook pattern correctly implements the modular compliance flow. The differentiation between regular transfers (AlwaysOn AND TransferOnly) and force transfers (AlwaysOn only) is well-designed. Using FHE.select to zero non-compliant transfers is an elegant FHE-native approach.

Based on learnings from ERC-3643 and PR comments.


189-205: LGTM!

The post-transfer hooks correctly execute after balance updates, allowing modules to track state (e.g., investor counts). The pattern aligns with ERC-3643's modular compliance approach.

Based on learnings from ERC-3643 and PR comments.


207-213: LGTM!

The handle access validation correctly restricts access to installed modules only, ensuring that only authorized compliance modules can query encrypted balances.

contracts/token/ERC7984/extensions/rwa/ERC7984RwaBalanceCapModule.sol (4)

17-19: LGTM!

The private encrypted balance cap with corresponding event follows good patterns for confidential state management.


25-36: LGTM!

The dual overloads for setting max balance (external with proof vs. internal encrypted) provide good flexibility. The onlyTokenAdmin restriction and FHE.allowThis calls are appropriate.


38-41: LGTM!

Simple getter for the encrypted max balance.


43-58: LGTM!

The balance cap compliance logic is well-implemented:

  • Burns are correctly exempted (line 49-51)
  • Uses tryIncrease to safely compute future balance and detect overflow
  • Checks both overflow protection and cap enforcement (line 56)

The unused from parameter is appropriate since balance caps only concern recipients.

contracts/interfaces/IERC7984Rwa.sol (3)

4-4: LGTM!

The FHE type imports are necessary for the new compliance module interfaces.


12-17: LGTM!

The role query functions provide necessary interfaces for compliance modules to validate permissions. The isAdminOrAgent convenience function reduces redundant checks.


71-84: LGTM!

The modular compliance interface provides a clean API for module management. The two-tier module system (AlwaysOn for all transfers, TransferOnly for regular transfers) offers good flexibility.

contracts/token/ERC7984/extensions/rwa/ERC7984RwaComplianceModule.sol (15)

1-3: LGTM!

License and pragma version are appropriate.


5-7: LGTM!

Imports are minimal and necessary for the compliance module implementation.


9-13: LGTM!

The contract correctly declares an abstract base with appropriate inheritance and immutable token binding.


15-22: LGTM!

Custom errors are well-defined and provide helpful debugging information.


24-28: LGTM!

The onlyToken modifier correctly restricts access to the token contract.


30-34: LGTM!

The onlyTokenAdmin modifier correctly delegates admin verification to the token contract.


36-40: LGTM!

The onlyTokenAgent modifier correctly delegates agent verification to the token contract.


42-46: LGTM!

The onlyTokenAdminOrTokenAgent modifier efficiently checks both roles with a single call.


52-55: LGTM!

The isModule function provides a standard mechanism for module identification via selector introspection.


67-69: LGTM!

The postTransfer hook correctly provides a post-transfer mutation point restricted to the token contract, aligning with the two-phase compliance check pattern (pre-check + post-hook) mentioned in the PR objectives.


72-76: LGTM!

The internal _isCompliantTransfer hook is correctly defined as an abstract method that derived modules must implement.


79-81: LGTM!

The default no-op implementation for _postTransfer is appropriate, allowing derived modules to opt-in to post-transfer state updates.


84-93: LGTM!

The handle allowance helpers correctly manage FHE handle access with appropriate initialization checks and flexible persistence options.


95-95: Ignore the empty override—base implementation is abstract. The base HandleAccessManager declares _validateHandleAllowance as an unimplemented abstract function, so this override—guarded by onlyTokenAdminOrTokenAgent—is the intended validation; no additional handle checks are required.

Likely an incorrect or invalid review comment.


57-64: No renaming—keep ERC-7984’s isCompliantTransfer and postTransfer
The module’s hook names must match the ERC-7984 interface (isCompliantTransfer/postTransfer), not ERC-3643’s canTransfer/transferred. Changing them would break the standard’s spec.

Likely an incorrect or invalid review comment.

Comment on lines +21 to +23
constructor(address token) ERC7984RwaComplianceModule(token) {
_token = token;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify redundant assignment.

Line 22 assigns _token = token, but the parent constructor ERC7984RwaComplianceModule(token) is called on line 21. If the parent constructor already sets _token, this assignment is redundant.

Check the parent contract to confirm:


🏁 Script executed:

#!/bin/bash
# Description: Check if ERC7984RwaComplianceModule constructor already sets _token

# Find the ERC7984RwaComplianceModule constructor
ast-grep --pattern $'contract ERC7984RwaComplianceModule {
  $$$
  constructor(address $PARAM) {
    $$$
  }
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Locate the ERC7984RwaComplianceModule contract
rg -n 'contract ERC7984RwaComplianceModule' -g 'contracts/token/ERC7984/extensions/rwa/ERC7984RwaComplianceModule.sol'

# Show lines around its constructor
rg -n 'constructor' -C3 -g 'contracts/token/ERC7984/extensions/rwa/ERC7984RwaComplianceModule.sol'

Length of output: 764


Remove redundant assignment in constructor
The parent ERC7984RwaComplianceModule constructor already sets _token, so the child’s _token = token; can be removed.

🤖 Prompt for AI Agents
In contracts/token/ERC7984/extensions/rwa/ERC7984RwaBalanceCapModule.sol around
lines 21 to 23, the constructor redundantly reassigns _token after calling the
parent ERC7984RwaComplianceModule(token) which already sets it; remove the line
"_token = token;" from the child constructor so the parent initialization is
relied upon and no duplicate assignment remains.

const alwaysOnType = 0;
const transferOnlyType = 1;
const moduleTypes = [alwaysOnType, transferOnlyType];
const maxInverstor = 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix typo in constant name.

The constant is named maxInverstor but should be maxInvestor. This typo is propagated to lines 30, 444, and 449.

Apply this diff to fix the typo:

-const maxInverstor = 2;
+const maxInvestor = 2;

Also update references on lines 30, 444, and 449.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const maxInverstor = 2;
const maxInvestor = 2;
🤖 Prompt for AI Agents
In test/token/ERC7984/extensions/ERC7984RwaModularCompliance.test.ts around line
12 (and refs at lines 30, 444, 449), the constant is misspelled as
`maxInverstor`; rename the declaration to `maxInvestor` and update all usages at
lines 30, 444, and 449 to use `maxInvestor` so the identifier is consistent and
correctly spelled throughout the file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants