Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
61729c2
Init `ERC7984Rwa` extension.
james-toussaint Aug 12, 2025
639e5a2
Fix typos
james-toussaint Aug 12, 2025
85546dd
Add agent role
james-toussaint Aug 12, 2025
0029ad7
Update spelling
james-toussaint Aug 12, 2025
b2174ea
Add pausable & roles tests
james-toussaint Aug 18, 2025
ce8286c
Add mint/burn/force/transfer tests
james-toussaint Aug 19, 2025
6cb98a5
Remove tmp freezable
james-toussaint Aug 25, 2025
46d6800
Merge remote-tracking branch 'origin/master' into feature/confidentia…
james-toussaint Aug 25, 2025
d2562aa
Name ERC7984Rwa
james-toussaint Aug 26, 2025
f58c1f3
Name confidential
james-toussaint Aug 26, 2025
76b21ae
Move RWA test
james-toussaint Aug 26, 2025
127aff5
Test with & without proof
james-toussaint Aug 26, 2025
84af687
Rwa mock uses freezable
james-toussaint Aug 26, 2025
b0d5ffa
Check transferred amounts in tests
james-toussaint Aug 26, 2025
6fb7f97
Bypass hardhat fhevm behaviour
james-toussaint Aug 26, 2025
0cb0208
Add support interface test
james-toussaint Aug 26, 2025
90ebfa0
Add should not force transfer if anyone
james-toussaint Aug 26, 2025
c5d07fe
Move some modifiers to mock
james-toussaint Aug 26, 2025
e484066
Update doc
james-toussaint Aug 26, 2025
4ec0bd1
Merge remote-tracking branch 'origin/master' into feature/confidentia…
james-toussaint Aug 29, 2025
64c6c9b
Swap items in doc
james-toussaint Aug 29, 2025
1ad9ccd
Add suggestions
james-toussaint Aug 29, 2025
628d143
Remove lint annotation
james-toussaint Aug 29, 2025
0b1d87c
Update test name
james-toussaint Aug 29, 2025
b6b6827
Add restriction to ERC7984Rwa
james-toussaint Sep 3, 2025
3185336
Move gates
james-toussaint Sep 3, 2025
b4f8c03
Remove ExpectedPause error
james-toussaint Sep 3, 2025
28973a2
Rename block functions
james-toussaint Sep 3, 2025
0facae5
Rename fixture
james-toussaint Sep 3, 2025
2d0ef0e
Force transfer with all update effects
james-toussaint Sep 4, 2025
5451777
Update set frozen doc
james-toussaint Sep 4, 2025
3065002
Refactor event checks in freezable tests
james-toussaint Sep 4, 2025
4a2e41e
Init compliance modules for confidential RWAs
james-toussaint Sep 5, 2025
3946b30
Add abstract compliance modules
james-toussaint Sep 5, 2025
4debb47
Compliance implements interface
james-toussaint Sep 5, 2025
86d5250
Add post transfer hook
james-toussaint Sep 8, 2025
30ca7df
Typo
james-toussaint Sep 8, 2025
57ab500
Init investor cap module
james-toussaint Sep 9, 2025
ed06057
Move rwa compliance contracts
james-toussaint Sep 9, 2025
4b4d558
Support confidential rwa module
james-toussaint Sep 10, 2025
4c45948
Rename rwa mock functions
james-toussaint Sep 10, 2025
9974fae
Immutable token in balance cap module
james-toussaint Sep 10, 2025
3332581
Switch to always-on/transfer-only compliance modules
james-toussaint Sep 11, 2025
c81b703
Typo
james-toussaint Sep 11, 2025
7d438c3
Use enum for compliance module type
james-toussaint Sep 11, 2025
fea22af
Enable token handles access to modules
james-toussaint Sep 15, 2025
b74c5ba
Increase coverage on modular compliance flow
james-toussaint Sep 16, 2025
b964c8b
Should not post update investors if not compliant
james-toussaint Sep 17, 2025
3be8b00
Rename to `ModularCompliance` & `ComplianceModule`
james-toussaint Sep 17, 2025
1bc4c3c
Add balance cap module tests
james-toussaint Sep 17, 2025
1336085
Add max investor tests
james-toussaint Sep 17, 2025
35b0771
Merge remote-tracking branch 'origin' into feature/confidential-rwa
james-toussaint Sep 17, 2025
4f04222
Use agent for operations
james-toussaint Sep 17, 2025
9e56fa5
Merge remote-tracking branch 'origin/feature/confidential-rwa' into f…
james-toussaint Sep 18, 2025
020fe73
Merge remote-tracking branch 'origin' into feature/confidential-rwa-c…
james-toussaint Sep 30, 2025
53ec926
Restore restricted and freezable
james-toussaint Sep 30, 2025
d3240e1
Update styling
james-toussaint Sep 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wet-results-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-confidential-contracts': minor
---

`ERC7984RwaModularCompliance`: Support compliance modules for confidential RWAs.
33 changes: 32 additions & 1 deletion contracts/interfaces/IERC7984Rwa.sol
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol";
import {ebool, externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol";
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
import {IERC7984} from "./IERC7984.sol";

/// @dev Interface for confidential RWA contracts.
interface IERC7984Rwa is IERC7984, IERC165 {
/// @dev Returns true if the contract is paused, false otherwise.
function paused() external view returns (bool);
/// @dev Returns true if has admin role, false otherwise.
function isAdmin(address account) external view returns (bool);
/// @dev Returns true if agent, false otherwise.
function isAgent(address account) external view returns (bool);
/// @dev Returns true if admin or agent, false otherwise.
function isAdminOrAgent(address account) external view returns (bool);
/// @dev Returns whether an account is allowed to interact with the token.
function isUserAllowed(address account) external view returns (bool);
/// @dev Returns the confidential frozen balance of an account.
Expand Down Expand Up @@ -61,3 +67,28 @@ interface IERC7984Rwa is IERC7984, IERC165 {
euint64 encryptedAmount
) external returns (euint64);
}

/// @dev Interface for confidential RWA with modular compliance.
interface IERC7984RwaModularCompliance {
enum ComplianceModuleType {
AlwaysOn,
TransferOnly
}

/// @dev Checks if a compliance module is installed.
function isModuleInstalled(ComplianceModuleType moduleType, address module) external view returns (bool);
/// @dev Installs a transfer compliance module.
function installModule(ComplianceModuleType moduleType, address module) external;
/// @dev Uninstalls a transfer compliance module.
function uninstallModule(ComplianceModuleType moduleType, address module) external;
}

/// @dev Interface for confidential RWA transfer compliance module.
interface IERC7984RwaComplianceModule {
/// @dev Returns magic number if it is a module.
function isModule() external returns (bytes4);
/// @dev Checks if a transfer is compliant. Should be non-mutating.
function isCompliantTransfer(address from, address to, euint64 encryptedAmount) external returns (ebool);
/// @dev Performs operation after transfer.
function postTransfer(address from, address to, euint64 encryptedAmount) external;
}
19 changes: 19 additions & 0 deletions contracts/mocks/token/ERC7984RwaBalanceCapModuleMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {FHE, euint64} from "@fhevm/solidity/lib/FHE.sol";
import {ERC7984RwaBalanceCapModule} from "../../token/ERC7984/extensions/rwa/ERC7984RwaBalanceCapModule.sol";

contract ERC7984RwaBalanceCapModuleMock is ERC7984RwaBalanceCapModule, SepoliaConfig {
event AmountEncrypted(euint64 amount);

constructor(address compliance) ERC7984RwaBalanceCapModule(compliance) {}

function createEncryptedAmount(uint64 amount) public returns (euint64 encryptedAmount) {
FHE.allowThis(encryptedAmount = FHE.asEuint64(amount));
FHE.allow(encryptedAmount, msg.sender);
emit AmountEncrypted(encryptedAmount);
}
}
38 changes: 38 additions & 0 deletions contracts/mocks/token/ERC7984RwaComplianceModuleMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {FHE, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol";
import {ERC7984RwaComplianceModule} from "../../token/ERC7984/extensions/rwa/ERC7984RwaComplianceModule.sol";

// solhint-disable func-name-mixedcase
contract ERC7984RwaModularComplianceModuleMock is ERC7984RwaComplianceModule, SepoliaConfig {
bool private _compliant = false;

event PostTransfer();
event PreTransfer();

constructor(address compliance) ERC7984RwaComplianceModule(compliance) {}

function $_setCompliant() public {
_compliant = true;
}

function $_unsetCompliant() public {
_compliant = false;
}

function _isCompliantTransfer(
address /*from*/,
address /*to*/,
euint64 /*encryptedAmount*/
) internal override returns (ebool) {
emit PreTransfer();
return FHE.asEbool(_compliant);
}

function _postTransfer(address /*from*/, address /*to*/, euint64 /*encryptedAmount*/) internal override {
emit PostTransfer();
}
}
10 changes: 10 additions & 0 deletions contracts/mocks/token/ERC7984RwaInvestorCapModuleMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {ERC7984RwaInvestorCapModule} from "../../token/ERC7984/extensions/rwa/ERC7984RwaInvestorCapModule.sol";

contract ERC7984RwaInvestorCapModuleMock is ERC7984RwaInvestorCapModule, SepoliaConfig {
constructor(address token, uint64 maxInvestor) ERC7984RwaInvestorCapModule(token, maxInvestor) {}
}
17 changes: 17 additions & 0 deletions contracts/mocks/token/ERC7984RwaModularComplianceMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {ERC7984} from "../../token/ERC7984/ERC7984.sol";
import {ERC7984Rwa} from "../../token/ERC7984/extensions/ERC7984Rwa.sol";
import {ERC7984RwaModularCompliance} from "../../token/ERC7984/extensions/rwa/ERC7984RwaModularCompliance.sol";

contract ERC7984RwaModularComplianceMock is ERC7984RwaModularCompliance, SepoliaConfig {
constructor(
string memory name,
string memory symbol,
string memory tokenUri,
address admin
) ERC7984Rwa(admin) ERC7984(name, symbol, tokenUri) {}
}
5 changes: 5 additions & 0 deletions contracts/token/ERC7984/extensions/ERC7984Rwa.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ abstract contract ERC7984Rwa is
return hasRole(AGENT_ROLE, account);
}

/// @dev Returns true if admin or agent, false otherwise.
function isAdminOrAgent(address account) public view virtual returns (bool) {
return isAdmin(account) || isAgent(account);
}

/// @dev Adds agent.
function addAgent(address account) public virtual onlyAdmin {
_grantRole(AGENT_ROLE, account);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {FHE, ebool, euint64, externalEuint64} from "@fhevm/solidity/lib/FHE.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {IERC7984} from "../../../../interfaces/IERC7984.sol";
import {FHESafeMath} from "../../../../utils/FHESafeMath.sol";
import {ERC7984RwaComplianceModule} from "./ERC7984RwaComplianceModule.sol";

/**
* @dev A transfer compliance module for confidential Real World Assets (RWAs) which limits the balance of an investor.
*/
abstract contract ERC7984RwaBalanceCapModule is ERC7984RwaComplianceModule {
using EnumerableSet for *;

euint64 private _maxBalance;

event MaxBalanceSet(euint64 newMaxBalance);

constructor(address token) ERC7984RwaComplianceModule(token) {
_token = token;
}
Comment on lines +21 to +23
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.


/// @dev Sets max balance of an investor with proof.
function setMaxBalance(externalEuint64 maxBalance, bytes calldata inputProof) public virtual onlyTokenAdmin {
euint64 maxBalance_ = FHE.fromExternal(maxBalance, inputProof);
FHE.allowThis(_maxBalance = maxBalance_);
emit MaxBalanceSet(maxBalance_);
}

/// @dev Sets max balance of an investor.
function setMaxBalance(euint64 maxBalance) public virtual onlyTokenAdmin {
FHE.allowThis(_maxBalance = maxBalance);
emit MaxBalanceSet(maxBalance);
}

/// @dev Gets max balance of an investor.
function getMaxBalance() public view virtual returns (euint64) {
return _maxBalance;
}

/// @dev Internal function which checks if a transfer is compliant.
function _isCompliantTransfer(
address /*from*/,
address to,
euint64 encryptedAmount
) internal override returns (ebool compliant) {
if (to == address(0)) {
return FHE.asEbool(true); // if burning
}
euint64 balance = IERC7984(_token).confidentialBalanceOf(to);
_getTokenHandleAllowance(balance);
_getTokenHandleAllowance(encryptedAmount);
(ebool increased, euint64 futureBalance) = FHESafeMath.tryIncrease(balance, encryptedAmount);
compliant = FHE.and(increased, FHE.le(futureBalance, _maxBalance));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {FHE, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol";
import {IERC7984Rwa, IERC7984RwaComplianceModule} from "../../../../interfaces/IERC7984Rwa.sol";
import {HandleAccessManager} from "../../../../utils/HandleAccessManager.sol";

/**
* @dev A contract which allows to build a transfer compliance module for confidential Real World Assets (RWAs).
*/
abstract contract ERC7984RwaComplianceModule is IERC7984RwaComplianceModule, HandleAccessManager {
address internal immutable _token;

/// @dev The sender is not the token.
error SenderNotToken(address account);
/// @dev The sender is not the token admin.
error SenderNotTokenAdmin(address account);
/// @dev The sender is not a token agent.
error SenderNotTokenAgent(address account);
/// @dev The sender is not the token admin or a token agent.
error SenderNotTokenAdminOrTokenAgent(address account);

/// @dev Throws if called by any account other than the token.
modifier onlyToken() {
require(msg.sender == _token, SenderNotToken(msg.sender));
_;
}

/// @dev Throws if called by any account other than the token admin.
modifier onlyTokenAdmin() {
require(IERC7984Rwa(_token).isAdmin(msg.sender), SenderNotTokenAdmin(msg.sender));
_;
}

/// @dev Throws if called by any account other than a token agent.
modifier onlyTokenAgent() {
require(IERC7984Rwa(_token).isAgent(msg.sender), SenderNotTokenAgent(msg.sender));
_;
}

/// @dev Throws if called by any account other than the token admin or a token agent.
modifier onlyTokenAdminOrTokenAgent() {
require(IERC7984Rwa(_token).isAdminOrAgent(msg.sender), SenderNotTokenAdminOrTokenAgent(msg.sender));
_;
}

constructor(address token) {
_token = token;
}

/// @inheritdoc IERC7984RwaComplianceModule
function isModule() public pure override returns (bytes4) {
return this.isModule.selector;
}

/// @inheritdoc IERC7984RwaComplianceModule
function isCompliantTransfer(
address from,
address to,
euint64 encryptedAmount
) public virtual onlyToken returns (ebool compliant) {
FHE.allow(compliant = _isCompliantTransfer(from, to, encryptedAmount), msg.sender);
}

/// @inheritdoc IERC7984RwaComplianceModule
function postTransfer(address from, address to, euint64 encryptedAmount) public virtual onlyToken {
_postTransfer(from, to, encryptedAmount);
}

/// @dev Internal function which checks if a transfer is compliant.
function _isCompliantTransfer(
address /*from*/,
address /*to*/,
euint64 /*encryptedAmount*/
) internal virtual returns (ebool);

/// @dev Internal function which performs operation after transfer.
function _postTransfer(address /*from*/, address /*to*/, euint64 /*encryptedAmount*/) internal virtual {
// default to no-op
}

/// @dev Allow modules to get access to token handles during transaction.
function _getTokenHandleAllowance(euint64 handle) internal virtual {
_getTokenHandleAllowance(handle, false);
}

/// @dev Allow modules to get access to token handles.
function _getTokenHandleAllowance(euint64 handle, bool persistent) internal virtual {
if (FHE.isInitialized(handle)) {
HandleAccessManager(_token).getHandleAllowance(euint64.unwrap(handle), address(this), persistent);
}
}

function _validateHandleAllowance(bytes32 handle) internal view override onlyTokenAdminOrTokenAgent {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {FHE, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol";
import {IERC7984} from "../../../../interfaces/IERC7984.sol";
import {ERC7984RwaComplianceModule} from "./ERC7984RwaComplianceModule.sol";

/**
* @dev A transfer compliance module for confidential Real World Assets (RWAs) which limits the number of investors.
*/
abstract contract ERC7984RwaInvestorCapModule is ERC7984RwaComplianceModule {
uint64 private _maxInvestor;
euint64 private _investors;

event MaxInvestorSet(uint64 maxInvestor);

constructor(address token, uint64 maxInvestor) ERC7984RwaComplianceModule(token) {
_maxInvestor = maxInvestor;
}

/// @dev Sets max number of investors.
function setMaxInvestor(uint64 maxInvestor) public virtual onlyTokenAdmin {
_maxInvestor = maxInvestor;
emit MaxInvestorSet(maxInvestor);
}

/// @dev Gets max number of investors.
function getMaxInvestor() public view virtual returns (uint64) {
return _maxInvestor;
}

/// @dev Gets current number of investors.
function getCurrentInvestor() public view virtual returns (euint64) {
return _investors;
}

/// @dev Internal function which checks if a transfer is compliant.
function _isCompliantTransfer(
address /*from*/,
address to,
euint64 encryptedAmount
) internal override returns (ebool compliant) {
euint64 balance = IERC7984(_token).confidentialBalanceOf(to);
_getTokenHandleAllowance(balance);
_getTokenHandleAllowance(encryptedAmount);
compliant = FHE.or(
FHE.or(
FHE.asEbool(to == address(0)), // return true if burning
FHE.eq(encryptedAmount, FHE.asEuint64(0)) // or zero amount
),
FHE.or(
FHE.gt(balance, FHE.asEuint64(0)), // or already investor
FHE.lt(_investors, FHE.asEuint64(_maxInvestor)) // or not reached max investors limit
)
);
}

/// @dev Internal function which performs operation after transfer.
function _postTransfer(address /*from*/, address to, euint64 encryptedAmount) internal override {
euint64 balance = IERC7984(_token).confidentialBalanceOf(to);
_getTokenHandleAllowance(balance);
_getTokenHandleAllowance(encryptedAmount);
if (!FHE.isInitialized(_investors)) {
_investors = FHE.asEuint64(0);
}
_investors = FHE.select(FHE.eq(balance, encryptedAmount), FHE.add(_investors, FHE.asEuint64(1)), _investors);
_investors = FHE.select(FHE.eq(balance, FHE.asEuint64(0)), FHE.sub(_investors, FHE.asEuint64(1)), _investors);
FHE.allowThis(_investors);
}
}
Loading