Skip to content
Open
Show file tree
Hide file tree
Changes from 42 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/new-crews-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-confidential-contracts': minor
---

`ERC7984Rwa`: An extension of `ERC7984`, that supports confidential Real World Assets (RWAs).
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
---

`ERC7984RwaCompliance`: Support compliance modules for confidential RWAs.
23 changes: 23 additions & 0 deletions contracts/interfaces/IERC7984Restricted.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

/// @dev Interface for contracts that implements user account transfer restrictions.
interface IERC7984Restricted {
enum Restriction {
DEFAULT, // User has no explicit restriction
BLOCKED, // User is explicitly blocked
ALLOWED // User is explicitly allowed
}

/// @dev Emitted when a user account's restriction is updated.
event UserRestrictionUpdated(address indexed account, Restriction restriction);

/// @dev The operation failed because the user account is restricted.
error UserRestricted(address account);

/// @dev Returns the restriction of a user account.
function getRestriction(address account) external view returns (Restriction);
/// @dev Returns whether a user account is allowed to interact with the token.
function isUserAllowed(address account) external view returns (bool);
}
105 changes: 105 additions & 0 deletions contracts/interfaces/IERC7984Rwa.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

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

uint256 constant TRANSFER_COMPLIANCE_MODULE_TYPE = 1;
uint256 constant FORCE_TRANSFER_COMPLIANCE_MODULE_TYPE = 2;

/// @dev Base interface for confidential RWA contracts.
interface IERC7984RwaBase {
/// @dev Emitted when the contract is paused.
event Paused(address account);
/// @dev Emitted when the contract is unpaused.
event Unpaused(address account);

/// @dev The caller account is not authorized to perform an operation.
error OwnableUnauthorizedAccount(address account);
/// @dev The owner is not a valid owner account. (eg. `address(0)`)
error OwnableInvalidOwner(address owner);
/// @dev The operation failed because the contract is paused.
error EnforcedPause();

/// @dev Returns true if the contract is paused, and false otherwise.
function paused() external view returns (bool);
/// @dev Pauses contract.
function pause() external;
/// @dev Unpauses contract.
function unpause() external;
/// @dev Returns the restriction of a user account.
function getRestriction(address account) external view returns (IERC7984Restricted.Restriction);
/// @dev Blocks a user account.
function blockUser(address account) external;
/// @dev Unblocks a user account.
function unblockUser(address account) external;
/// @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.
function confidentialFrozen(address account) external view returns (euint64);
/// @dev Returns the available (unfrozen) balance of an account. Up to {confidentialBalanceOf}.
function confidentialAvailable(address account) external returns (euint64);
/// @dev Sets confidential amount of token for an account as frozen with proof.
function setConfidentialFrozen(
address account,
externalEuint64 encryptedAmount,
bytes calldata inputProof
) external;
/// @dev Sets confidential amount of token for an account as frozen.
function setConfidentialFrozen(address account, euint64 encryptedAmount) external;
/// @dev Mints confidential amount of tokens to account with proof.
function confidentialMint(
address to,
externalEuint64 encryptedAmount,
bytes calldata inputProof
) external returns (euint64);
/// @dev Mints confidential amount of tokens to account.
function confidentialMint(address to, euint64 encryptedAmount) external returns (euint64);
/// @dev Burns confidential amount of tokens from account with proof.
function confidentialBurn(
address account,
externalEuint64 encryptedAmount,
bytes calldata inputProof
) external returns (euint64);
/// @dev Burns confidential amount of tokens from account.
function confidentialBurn(address account, euint64 encryptedAmount) external returns (euint64);
/// @dev Forces transfer of confidential amount of tokens from account to account with proof by skipping compliance checks.
function forceConfidentialTransferFrom(
address from,
address to,
externalEuint64 encryptedAmount,
bytes calldata inputProof
) external returns (euint64);
/// @dev Forces transfer of confidential amount of tokens from account to account by skipping compliance checks.
function forceConfidentialTransferFrom(
address from,
address to,
euint64 encryptedAmount
) external returns (euint64);
/// @dev Receives and executes a batch of function calls on this contract.
function multicall(bytes[] calldata data) external returns (bytes[] memory results);
}

/// @dev Full interface for confidential RWA contracts.
interface IERC7984Rwa is IERC7984, IERC7984RwaBase, IERC165, IAccessControl {}

/// @dev Interface for confidential RWA compliance.
interface IERC7984RwaCompliance {
/// @dev Installs a transfer compliance module.
function installModule(uint256 moduleTypeId, address module) external;
/// @dev Uninstalls a transfer compliance module.
function uninstallModule(uint256 moduleTypeId, address module) external;
}

/// @dev Interface for confidential RWA transfer compliance module.
interface IERC7984RwaTransferComplianceModule {
/// @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 postTransferHook(address from, address to, euint64 encryptedAmount) external;
}
76 changes: 76 additions & 0 deletions contracts/mocks/token/ERC7984RwaMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {FHE, ebool, euint64, externalEuint64} from "@fhevm/solidity/lib/FHE.sol";
import {Impl} from "@fhevm/solidity/lib/Impl.sol";
import {ERC7984Rwa} from "../../token/ERC7984/extensions/ERC7984Rwa.sol";
import {FHESafeMath} from "../../utils/FHESafeMath.sol";
import {HandleAccessManager} from "../../utils/HandleAccessManager.sol";

// solhint-disable func-name-mixedcase
contract ERC7984RwaMock is ERC7984Rwa, HandleAccessManager, SepoliaConfig {
mapping(address account => euint64 encryptedAmount) private _frozenBalances;
bool public compliantTransfer = false;
bool public compliantForceTransfer = false;

// TODO: Move modifiers to `ERC7984Rwa` or remove from mock if useless
/// @dev Checks if the sender is an admin.
modifier onlyAdmin() {
require(isAdmin(_msgSender()), UnauthorizedSender(_msgSender()));
_;
}
/// @dev Checks if the sender is an agent.
modifier onlyAgent() {
require(isAgent(_msgSender()), UnauthorizedSender(_msgSender()));
_;
}

constructor(string memory name, string memory symbol, string memory tokenUri) ERC7984Rwa(name, symbol, tokenUri) {}

function createEncryptedAmount(uint64 amount) public returns (euint64 encryptedAmount) {
FHE.allowThis(encryptedAmount = FHE.asEuint64(amount));
FHE.allow(encryptedAmount, msg.sender);
}

function $_setCompliantTransfer() public {
compliantTransfer = true;
}

function $_unsetCompliantTransfer() public {
compliantTransfer = false;
}

function $_setCompliantForceTransfer() public {
compliantForceTransfer = true;
}

function $_unsetCompliantForceTransfer() public {
compliantForceTransfer = false;
}

function $_mint(address to, uint64 amount) public returns (euint64 transferred) {
return _mint(to, FHE.asEuint64(amount));
}

function _isTransferCompliant(
address /*from*/,
address /*to*/,
euint64 /*encryptedAmount*/
) internal override returns (ebool compliant) {
compliant = FHE.asEbool(compliantTransfer);
FHE.allowThis(compliant);
}

function _isForceTransferCompliant(
address /*from*/,
address /*to*/,
euint64 /*encryptedAmount*/
) internal override returns (ebool compliant) {
compliant = FHE.asEbool(compliantForceTransfer);
FHE.allowThis(compliant);
}

function _validateHandleAllowance(bytes32 handle) internal view override onlyAdminOrAgent {}
}
46 changes: 42 additions & 4 deletions contracts/token/ERC7984/extensions/ERC7984Freezable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {ERC7984} from "../ERC7984.sol";
abstract contract ERC7984Freezable is ERC7984 {
/// @dev Confidential frozen amount of tokens per address.
mapping(address account => euint64 encryptedAmount) private _frozenBalances;
/// @dev Skips frozen checks in {_update}.
bool private _skipUpdateCheck;

/// @dev Emitted when a confidential amount of token is frozen for an account
event TokensFrozen(address indexed account, euint64 encryptedAmount);
Expand All @@ -36,6 +38,9 @@ abstract contract ERC7984Freezable is ERC7984 {
confidentialBalanceOf(account),
confidentialFrozen(account)
);
if (!FHE.isInitialized(unfrozen)) {
return unfrozen;
}
return FHE.select(success, unfrozen, FHE.asEuint64(0));
}

Expand All @@ -59,7 +64,14 @@ abstract contract ERC7984Freezable is ERC7984 {

/// @dev Internal function to freeze a confidential amount of tokens for an account.
function _setConfidentialFrozen(address account, euint64 encryptedAmount) internal virtual {
_checkFreezer();
_setConfidentialFrozen(account, encryptedAmount, true);
}

/// @dev Private function to freeze a confidential amount of tokens for an account with optional freezer check.
function _setConfidentialFrozen(address account, euint64 encryptedAmount, bool checkFreezer) internal virtual {
if (checkFreezer) {
_checkFreezer();
}
FHE.allowThis(encryptedAmount);
FHE.allow(encryptedAmount, account);
_frozenBalances[account] = encryptedAmount;
Expand All @@ -69,15 +81,41 @@ abstract contract ERC7984Freezable is ERC7984 {
/// @dev Unimplemented function that must revert if `msg.sender` is not authorized as a freezer.
function _checkFreezer() internal virtual;

/// @dev Internal function to skip update check. Check can be restored with {_restoreERC7984FreezableUpdateCheck}.
function _disableERC7984FreezableUpdateCheck() internal virtual {
if (!_skipUpdateCheck) {
_skipUpdateCheck = true;
}
}

/// @dev Internal function to restore update check previously disabled by {_disableERC7984FreezableUpdateCheck}.
function _restoreERC7984FreezableUpdateCheck() internal virtual {
if (_skipUpdateCheck) {
_skipUpdateCheck = false;
}
}

/**
* @dev See {ERC7984-_update}. The `from` account must have sufficient unfrozen balance,
* otherwise 0 tokens are transferred.
*/
function _update(address from, address to, euint64 encryptedAmount) internal virtual override returns (euint64) {
euint64 available;
if (from != address(0)) {
euint64 unfrozen = confidentialAvailable(from);
encryptedAmount = FHE.select(FHE.le(encryptedAmount, unfrozen), encryptedAmount, FHE.asEuint64(0));
available = confidentialAvailable(from);
if (!_skipUpdateCheck) {
encryptedAmount = FHE.select(FHE.le(encryptedAmount, available), encryptedAmount, FHE.asEuint64(0));
}
}
euint64 transferred = super._update(from, to, encryptedAmount);
if (from != address(0) && _skipUpdateCheck) {
// Reset frozen to balance if transferred more than available
_setConfidentialFrozen(
from,
FHE.select(FHE.gt(transferred, available), confidentialBalanceOf(from), confidentialFrozen(from)),
false
);
}
return super._update(from, to, encryptedAmount);
return transferred;
}
}
37 changes: 22 additions & 15 deletions contracts/token/ERC7984/extensions/ERC7984Restricted.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

pragma solidity ^0.8.27;

import {IERC7984Restricted} from "../../../interfaces/IERC7984Restricted.sol";
import {ERC7984, euint64} from "../ERC7984.sol";

/**
Expand All @@ -13,20 +14,10 @@ import {ERC7984, euint64} from "../ERC7984.sol";
* a blocklist. Developers can override {isUserAllowed} to check that `restriction == ALLOWED`
* to implement an allowlist.
*/
abstract contract ERC7984Restricted is ERC7984 {
enum Restriction {
DEFAULT, // User has no explicit restriction
BLOCKED, // User is explicitly blocked
ALLOWED // User is explicitly allowed
}

abstract contract ERC7984Restricted is ERC7984, IERC7984Restricted {
mapping(address account => Restriction) private _restrictions;

/// @dev Emitted when a user account's restriction is updated.
event UserRestrictionUpdated(address indexed account, Restriction restriction);

/// @dev The operation failed because the user account is restricted.
error UserRestricted(address account);
/// @dev Skips restriction checks in {_update}.
bool private _skipUpdateCheck;

/// @dev Returns the restriction of a user account.
function getRestriction(address account) public view virtual returns (Restriction) {
Expand All @@ -50,6 +41,20 @@ abstract contract ERC7984Restricted is ERC7984 {
return getRestriction(account) != Restriction.BLOCKED; // i.e. DEFAULT && ALLOWED
}

/// @dev Internal function to skip update check. Check can be restored with {_restoreERC7984RestrictedUpdateCheck}.
function _disableERC7984RestrictedUpdateCheck() internal virtual {
if (!_skipUpdateCheck) {
_skipUpdateCheck = true;
}
}

/// @dev Internal function to restore update check previously disabled by {_disableERC7984RestrictedUpdateCheck}.
function _restoreERC7984RestrictedUpdateCheck() internal virtual {
if (_skipUpdateCheck) {
_skipUpdateCheck = false;
}
}

/**
* @dev See {ERC7984-_update}. Enforces transfer restrictions (excluding minting and burning).
*
Expand All @@ -59,8 +64,10 @@ abstract contract ERC7984Restricted is ERC7984 {
* * `to` must be allowed to receive tokens (see {isUserAllowed}).
*/
function _update(address from, address to, euint64 value) internal virtual override returns (euint64) {
if (from != address(0)) _checkRestriction(from); // Not minting
if (to != address(0)) _checkRestriction(to); // Not burning
if (!_skipUpdateCheck) {
if (from != address(0)) _checkRestriction(from); // Not minting
if (to != address(0)) _checkRestriction(to); // Not burning
}
return super._update(from, to, value);
}

Expand Down
Loading