@openzeppelin/confidential-contracts 0.4.0 → 0.5.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/build/contracts/BatcherConfidential.json +5 -0
  2. package/build/contracts/CheckpointsConfidential.json +2 -2
  3. package/build/contracts/ERC7984.json +6 -28
  4. package/build/contracts/ERC7984BalanceCapHookModule.json +277 -0
  5. package/build/contracts/ERC7984ERC20Wrapper.json +6 -28
  6. package/build/contracts/ERC7984Freezable.json +6 -28
  7. package/build/contracts/ERC7984HolderCapHookModule.json +291 -0
  8. package/build/contracts/ERC7984HookModule.json +200 -0
  9. package/build/contracts/ERC7984Hooked.json +832 -0
  10. package/build/contracts/ERC7984IdentityCheck.json +691 -0
  11. package/build/contracts/ERC7984ObserverAccess.json +6 -28
  12. package/build/contracts/ERC7984Omnibus.json +6 -28
  13. package/build/contracts/ERC7984Restricted.json +6 -28
  14. package/build/contracts/ERC7984Rwa.json +61 -29
  15. package/build/contracts/ERC7984Utils.json +2 -2
  16. package/build/contracts/ERC7984Votes.json +6 -28
  17. package/build/contracts/FHESafeMath.json +2 -2
  18. package/build/contracts/IERC7984HookModule.json +151 -0
  19. package/build/contracts/IERC7984Rwa.json +87 -0
  20. package/build/contracts/IIdentityRegistry.json +30 -0
  21. package/finance/BatcherConfidential.sol +7 -3
  22. package/governance/utils/VotesConfidential.sol +2 -2
  23. package/interfaces/IERC7984HookModule.sol +39 -0
  24. package/interfaces/IERC7984Receiver.sol +3 -1
  25. package/interfaces/IERC7984Rwa.sol +28 -1
  26. package/package.json +1 -1
  27. package/token/ERC7984/ERC7984.sol +39 -28
  28. package/token/ERC7984/extensions/ERC7984ERC20Wrapper.sol +3 -3
  29. package/token/ERC7984/extensions/ERC7984Freezable.sol +3 -7
  30. package/token/ERC7984/extensions/ERC7984Hooked.sol +158 -0
  31. package/token/ERC7984/extensions/ERC7984IdentityCheck.sol +58 -0
  32. package/token/ERC7984/extensions/ERC7984Restricted.sol +3 -3
  33. package/token/ERC7984/extensions/ERC7984Rwa.sol +65 -28
  34. package/token/ERC7984/utils/ERC7984BalanceCapHookModule.sol +92 -0
  35. package/token/ERC7984/utils/ERC7984HolderCapHookModule.sol +145 -0
  36. package/token/ERC7984/utils/ERC7984HookModule.sol +170 -0
  37. package/utils/FHESafeMath.sol +26 -1
  38. package/utils/HandleAccessManager.sol +5 -3
  39. package/utils/structs/CheckpointsConfidential.sol +1 -2
@@ -0,0 +1,158 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Confidential Contracts (last updated v0.5.0-rc.0) (token/ERC7984/extensions/ERC7984Hooked.sol)
3
+
4
+ pragma solidity ^0.8.27;
5
+
6
+ import {FHE, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol";
7
+ import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
8
+ import {LowLevelCall} from "@openzeppelin/contracts/utils/LowLevelCall.sol";
9
+ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
10
+ import {IERC7984HookModule} from "./../../../interfaces/IERC7984HookModule.sol";
11
+ import {HandleAccessManager} from "./../../../utils/HandleAccessManager.sol";
12
+ import {ERC7984} from "./../ERC7984.sol";
13
+
14
+ /**
15
+ * @dev Extension of {ERC7984} that supports hook modules. Inspired by ERC-7579 modules.
16
+ *
17
+ * Modules are called before and after transfers. Before the transfer, modules
18
+ * conduct checks to see if they approve the given transfer and return an encrypted boolean. If any module
19
+ * returns false, the transferred amount becomes 0. After the transfer, modules are notified of the final transfer
20
+ * amount and may do accounting as necessary. Modules may revert on either call, which will propagate
21
+ * and revert the entire transaction.
22
+ *
23
+ * NOTE: Hook modules are trusted contracts--they have access to any private state the token has access to.
24
+ */
25
+ abstract contract ERC7984Hooked is ERC7984, HandleAccessManager {
26
+ using EnumerableSet for *;
27
+
28
+ EnumerableSet.AddressSet private _modules;
29
+
30
+ /// @dev Emitted when a module is installed.
31
+ event ERC7984HookedModuleInstalled(address module);
32
+ /// @dev Emitted when a module is uninstalled.
33
+ event ERC7984HookedModuleUninstalled(address module);
34
+
35
+ /// @dev The address is not a valid module.
36
+ error ERC7984HookedInvalidModule(address module);
37
+ /// @dev The module is already installed.
38
+ error ERC7984HookedDuplicateModule(address module);
39
+ /// @dev The module is not installed.
40
+ error ERC7984HookedNonexistentModule(address module);
41
+ /// @dev The maximum number of modules has been exceeded.
42
+ error ERC7984HookedExceededMaxModules();
43
+
44
+ modifier onlyAuthorizedModuleChange() {
45
+ _authorizeModuleChange();
46
+ _;
47
+ }
48
+
49
+ /// @dev Checks if a module is installed.
50
+ function isModuleInstalled(address module) public view virtual returns (bool) {
51
+ return _modules.contains(module);
52
+ }
53
+
54
+ /**
55
+ * @dev Installs a hook module.
56
+ *
57
+ * Consider gas footprint of the module before adding it since all modules will perform
58
+ * both steps (pre-hook, post-hook) on all transfers.
59
+ */
60
+ function installModule(address module, bytes memory initData) public virtual onlyAuthorizedModuleChange {
61
+ _installModule(module, initData);
62
+ }
63
+
64
+ /// @dev Uninstalls a hook module.
65
+ function uninstallModule(address module, bytes memory deinitData) public virtual onlyAuthorizedModuleChange {
66
+ _uninstallModule(module, deinitData);
67
+ }
68
+
69
+ /**
70
+ * @dev Returns a slice of the list of modules installed on the token with inclusive start and exclusive end.
71
+ *
72
+ * TIP: Use an end value of type(uint256).max to get the entire list of modules.
73
+ */
74
+ function modules(uint256 start, uint256 end) public view virtual returns (address[] memory) {
75
+ return _modules.values(start, end);
76
+ }
77
+
78
+ /// @dev Returns the maximum number of modules that can be installed.
79
+ function maxModules() public view virtual returns (uint256) {
80
+ return 15;
81
+ }
82
+
83
+ /// @dev Authorization logic for installing and uninstalling modules. Must be implemented by the concrete contract.
84
+ function _authorizeModuleChange() internal virtual;
85
+
86
+ /// @dev Internal function which installs a hook module.
87
+ function _installModule(address module, bytes memory initData) internal virtual {
88
+ require(_modules.length() < maxModules(), ERC7984HookedExceededMaxModules());
89
+ require(
90
+ ERC165Checker.supportsInterface(module, type(IERC7984HookModule).interfaceId),
91
+ ERC7984HookedInvalidModule(module)
92
+ );
93
+ require(_modules.add(module), ERC7984HookedDuplicateModule(module));
94
+
95
+ IERC7984HookModule(module).onInstall(initData);
96
+
97
+ emit ERC7984HookedModuleInstalled(module);
98
+ }
99
+
100
+ /// @dev Internal function which uninstalls a module.
101
+ function _uninstallModule(address module, bytes memory deinitData) internal virtual {
102
+ require(_modules.remove(module), ERC7984HookedNonexistentModule(module));
103
+
104
+ LowLevelCall.callNoReturn(module, abi.encodeCall(IERC7984HookModule.onUninstall, (deinitData)));
105
+
106
+ emit ERC7984HookedModuleUninstalled(module);
107
+ }
108
+
109
+ /**
110
+ * @dev See {ERC7984-_update}.
111
+ *
112
+ * Modified to run pre and post transfer hooks. Zero tokens are transferred if a module does not approve
113
+ * the transfer.
114
+ */
115
+ function _update(
116
+ address from,
117
+ address to,
118
+ euint64 encryptedAmount
119
+ ) internal virtual override returns (euint64 transferred) {
120
+ euint64 amountToTransfer = FHE.select(
121
+ _runPreTransferHooks(from, to, encryptedAmount),
122
+ encryptedAmount,
123
+ FHE.asEuint64(0)
124
+ );
125
+ transferred = super._update(from, to, amountToTransfer);
126
+ _runPostTransferHooks(from, to, transferred);
127
+ }
128
+
129
+ /// @dev Runs the pre-transfer hooks for all modules.
130
+ function _runPreTransferHooks(
131
+ address from,
132
+ address to,
133
+ euint64 encryptedAmount
134
+ ) internal virtual returns (ebool compliant) {
135
+ address[] memory modules_ = modules(0, type(uint256).max);
136
+ uint256 modulesLength = modules_.length;
137
+ compliant = FHE.asEbool(true);
138
+ for (uint256 i = 0; i < modulesLength; ++i) {
139
+ if (FHE.isInitialized(encryptedAmount)) FHE.allowTransient(encryptedAmount, modules_[i]);
140
+ compliant = FHE.and(compliant, IERC7984HookModule(modules_[i]).preTransfer(from, to, encryptedAmount));
141
+ }
142
+ }
143
+
144
+ /// @dev Runs the post-transfer hooks for all modules.
145
+ function _runPostTransferHooks(address from, address to, euint64 encryptedAmount) internal virtual {
146
+ address[] memory modules_ = modules(0, type(uint256).max);
147
+ uint256 modulesLength = modules_.length;
148
+ for (uint256 i = 0; i < modulesLength; i++) {
149
+ if (FHE.isInitialized(encryptedAmount)) FHE.allowTransient(encryptedAmount, modules_[i]);
150
+ IERC7984HookModule(modules_[i]).postTransfer(from, to, encryptedAmount);
151
+ }
152
+ }
153
+
154
+ /// @dev See {HandleAccessManager-_validateHandleAllowance}. Allow modules to access any handle the token has access to.
155
+ function _validateHandleAllowance(bytes32 handle) internal view virtual override returns (bool) {
156
+ return super._validateHandleAllowance(handle) || _modules.contains(msg.sender);
157
+ }
158
+ }
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Confidential Contracts (last updated v0.5.0-rc.0) (token/ERC7984/extensions/ERC7984IdentityCheck.sol)
3
+
4
+ pragma solidity ^0.8.27;
5
+
6
+ import {euint64} from "@fhevm/solidity/lib/FHE.sol";
7
+ import {ERC7984} from "../ERC7984.sol";
8
+
9
+ /**
10
+ * @dev Extension of {ERC7984} that enforces identity verification
11
+ * on token recipients by querying an external identity registry.
12
+ *
13
+ * See https://github.com/ERC-3643/ERC-3643/blob/main/contracts/registry/interface/IIdentityRegistry.sol[IIdentityRegistry]
14
+ * for more information.
15
+ */
16
+ abstract contract ERC7984IdentityCheck is ERC7984 {
17
+ /// @dev Emitted when the identity registry is updated.
18
+ event IdentityRegistryUpdated(address indexed oldRegistry, address indexed newRegistry);
19
+
20
+ /// @dev The provided registry address is invalid.
21
+ error ERC7984InvalidIdentityRegistry(address registry);
22
+
23
+ /// @dev The `account` is not verified in the identity registry.
24
+ error ERC7984InvalidIdentity(address account);
25
+
26
+ address private _identityRegistry;
27
+
28
+ constructor(address identityRegistry_) {
29
+ _setIdentityRegistry(identityRegistry_);
30
+ }
31
+
32
+ /// @dev See {ERC7984-_update}. Performs identity check on the recipient before updating the balance.
33
+ function _update(address from, address to, euint64 amount) internal virtual override returns (euint64) {
34
+ if (to != address(0) && !IIdentityRegistry(_identityRegistry).isVerified(to)) {
35
+ revert ERC7984InvalidIdentity(to);
36
+ }
37
+ return super._update(from, to, amount);
38
+ }
39
+
40
+ /// @dev Returns the address of the identity registry.
41
+ function identityRegistry() public view virtual returns (address) {
42
+ return _identityRegistry;
43
+ }
44
+
45
+ /// @dev Sets the identity registry. Inheriting contracts are responsible for access control.
46
+ function _setIdentityRegistry(address identityRegistry_) internal virtual {
47
+ require(
48
+ identityRegistry_ != address(0) && identityRegistry_.code.length != 0,
49
+ ERC7984InvalidIdentityRegistry(identityRegistry_)
50
+ );
51
+ emit IdentityRegistryUpdated(_identityRegistry, identityRegistry_);
52
+ _identityRegistry = identityRegistry_;
53
+ }
54
+ }
55
+
56
+ interface IIdentityRegistry {
57
+ function isVerified(address account) external view returns (bool);
58
+ }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Confidential Contracts (last updated v0.4.0) (token/ERC7984/extensions/ERC7984Restricted.sol)
2
+ // OpenZeppelin Confidential Contracts (last updated v0.5.0-rc.0) (token/ERC7984/extensions/ERC7984Restricted.sol)
3
3
 
4
4
  pragma solidity ^0.8.27;
5
5
 
@@ -35,7 +35,7 @@ abstract contract ERC7984Restricted is ERC7984 {
35
35
  }
36
36
 
37
37
  /**
38
- * @dev Returns whether a user account is allowed to interact with the token.
38
+ * @dev Returns whether a user account is allowed to receive or send tokens.
39
39
  *
40
40
  * Default implementation only disallows explicitly BLOCKED accounts (i.e. a blocklist).
41
41
  */
@@ -44,7 +44,7 @@ abstract contract ERC7984Restricted is ERC7984 {
44
44
  }
45
45
 
46
46
  /**
47
- * @dev See {ERC7984-_update}. Enforces transfer restrictions (excluding minting and burning).
47
+ * @dev See {ERC7984-_update}. Enforces transfer restrictions.
48
48
  *
49
49
  * Requirements:
50
50
  *
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // OpenZeppelin Confidential Contracts (last updated v0.4.0) (token/ERC7984/extensions/ERC7984Rwa.sol)
2
+ // OpenZeppelin Confidential Contracts (last updated v0.5.0-rc.0) (token/ERC7984/extensions/ERC7984Rwa.sol)
3
3
 
4
4
  pragma solidity ^0.8.27;
5
5
 
@@ -9,8 +9,8 @@ import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
9
9
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
10
10
  import {Multicall} from "@openzeppelin/contracts/utils/Multicall.sol";
11
11
  import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
12
- import {IERC7984} from "./../../../interfaces/IERC7984.sol";
13
12
  import {IERC7984Rwa} from "./../../../interfaces/IERC7984Rwa.sol";
13
+ import {FHESafeMath} from "./../../../utils/FHESafeMath.sol";
14
14
  import {ERC7984} from "./../ERC7984.sol";
15
15
  import {ERC7984Freezable} from "./ERC7984Freezable.sol";
16
16
  import {ERC7984Restricted} from "./ERC7984Restricted.sol";
@@ -20,15 +20,18 @@ import {ERC7984Restricted} from "./ERC7984Restricted.sol";
20
20
  * This interface provides compliance checks, transfer controls and enforcement actions.
21
21
  */
22
22
  abstract contract ERC7984Rwa is IERC7984Rwa, ERC7984Freezable, ERC7984Restricted, Pausable, Multicall, AccessControl {
23
+ /// @dev The operation failed because the lost account is the same as the new account.
24
+ error ERC7984RwaSelfRecoveryNotAllowed();
25
+
23
26
  /**
24
27
  * @dev Accounts granted the agent role have the following permissioned abilities:
25
28
  *
26
- * - Mint/Burn to/from a given address (does not require permission)
27
- * - Force transfer from a given address (does not require permission)
28
- * - Bypasses pause and restriction checks (not frozen)
29
- * - Pause/Unpause the contract
30
- * - Block/Unblock a given account
31
- * - Set frozen amount of tokens for a given account.
29
+ * * Mint/Burn to/from a given address (does not require permission)
30
+ * * Force transfer from a given address (does not require permission)
31
+ * ** Bypasses pause and restriction checks (not frozen)
32
+ * * Pause/Unpause the contract
33
+ * * Block/Unblock a given account
34
+ * * Set frozen amount of tokens for a given account.
32
35
  */
33
36
  bytes32 public constant AGENT_ROLE = keccak256("AGENT_ROLE");
34
37
 
@@ -157,6 +160,38 @@ abstract contract ERC7984Rwa is IERC7984Rwa, ERC7984Freezable, ERC7984Restricted
157
160
  return burntAmount;
158
161
  }
159
162
 
163
+ /// @inheritdoc IERC7984Rwa
164
+ function recoverAddress(address lostAccount, address newAccount) public virtual onlyAgent returns (euint64) {
165
+ require(lostAccount != newAccount, ERC7984RwaSelfRecoveryNotAllowed());
166
+
167
+ euint64 balance = confidentialBalanceOf(lostAccount);
168
+ euint64 lostFrozenBalance = confidentialFrozen(lostAccount);
169
+
170
+ if (FHE.isInitialized(lostFrozenBalance)) {
171
+ _setConfidentialFrozen(lostAccount, euint64.wrap(0));
172
+ }
173
+
174
+ euint64 tokensRecovered = _transfer(lostAccount, newAccount, balance);
175
+ FHE.allow(tokensRecovered, msg.sender);
176
+
177
+ if (FHE.isInitialized(lostFrozenBalance)) {
178
+ _setConfidentialFrozen(
179
+ newAccount,
180
+ FHESafeMath.saturatingAdd(confidentialFrozen(newAccount), FHE.min(tokensRecovered, lostFrozenBalance))
181
+ );
182
+ _setConfidentialFrozen(lostAccount, FHESafeMath.saturatingSub(lostFrozenBalance, tokensRecovered));
183
+ }
184
+
185
+ Restriction restriction = getRestriction(lostAccount);
186
+ if (restriction == Restriction.BLOCKED) {
187
+ _blockUser(newAccount);
188
+ }
189
+
190
+ emit TokensRecovered(lostAccount, newAccount, tokensRecovered);
191
+
192
+ return tokensRecovered;
193
+ }
194
+
160
195
  /// @dev Variant of {forceConfidentialTransferFrom-address-address-euint64} with an input proof.
161
196
  function forceConfidentialTransferFrom(
162
197
  address from,
@@ -164,7 +199,9 @@ abstract contract ERC7984Rwa is IERC7984Rwa, ERC7984Freezable, ERC7984Restricted
164
199
  externalEuint64 encryptedAmount,
165
200
  bytes calldata inputProof
166
201
  ) public virtual onlyAgent returns (euint64) {
167
- return _forceUpdate(from, to, FHE.fromExternal(encryptedAmount, inputProof));
202
+ euint64 transferred = _transfer(from, to, FHE.fromExternal(encryptedAmount, inputProof));
203
+ FHE.allow(transferred, msg.sender);
204
+ return transferred;
168
205
  }
169
206
 
170
207
  /**
@@ -176,12 +213,14 @@ abstract contract ERC7984Rwa is IERC7984Rwa, ERC7984Freezable, ERC7984Restricted
176
213
  address from,
177
214
  address to,
178
215
  euint64 encryptedAmount
179
- ) public virtual onlyAgent returns (euint64 transferred) {
216
+ ) public virtual onlyAgent returns (euint64) {
180
217
  require(
181
218
  FHE.isAllowed(encryptedAmount, msg.sender),
182
219
  ERC7984UnauthorizedUseOfEncryptedAmount(encryptedAmount, msg.sender)
183
220
  );
184
- return _forceUpdate(from, to, encryptedAmount);
221
+ euint64 transferred = _transfer(from, to, encryptedAmount);
222
+ FHE.allow(transferred, msg.sender);
223
+ return transferred;
185
224
  }
186
225
 
187
226
  /// @inheritdoc ERC7984Freezable
@@ -218,29 +257,27 @@ abstract contract ERC7984Rwa is IERC7984Rwa, ERC7984Freezable, ERC7984Restricted
218
257
  return super._update(from, to, encryptedAmount);
219
258
  }
220
259
 
221
- /// @dev Internal function which forces transfer of confidential amount of tokens from account to account by skipping compliance checks.
222
- function _forceUpdate(address from, address to, euint64 encryptedAmount) internal virtual returns (euint64) {
223
- // bypassing `from` restriction check with {_checkSenderRestriction}. Still performing `to` restriction check.
224
- // bypassing paused state by directly calling `super._update`
225
- euint64 transferred = super._update(from, to, encryptedAmount);
226
- FHE.allow(transferred, msg.sender);
227
- return transferred;
228
- }
229
-
230
- /**
231
- * @dev Bypasses the `from` restriction check when performing a {forceConfidentialTransferFrom}.
232
- */
260
+ /// @dev Bypasses {ERC7984Restricted} `from` restriction check when performing a {forceConfidentialTransferFrom}.
233
261
  function _checkSenderRestriction(address account) internal view override {
234
- if (_isForceTransfer()) {
262
+ if (_isForceTransfer(msg.sig)) {
235
263
  return;
236
264
  }
237
265
  super._checkSenderRestriction(account);
238
266
  }
239
267
 
240
- /// @dev Private function which checks if the called function is a {forceConfidentialTransferFrom}.
241
- function _isForceTransfer() private pure returns (bool) {
268
+ /// @dev Bypasses {Pausable} check when performing a {forceConfidentialTransferFrom}.
269
+ function _requireNotPaused() internal view override {
270
+ if (_isForceTransfer(msg.sig)) {
271
+ return;
272
+ }
273
+ super._requireNotPaused();
274
+ }
275
+
276
+ /// @dev Internal function which checks if the current function call should be treated as a force transfer.
277
+ function _isForceTransfer(bytes4 selector) internal pure returns (bool) {
242
278
  return
243
- msg.sig == 0x6c9c3c85 || // bytes4(keccak256("forceConfidentialTransferFrom(address,address,bytes32,bytes)"))
244
- msg.sig == 0x44fd6e40; // bytes4(keccak256("forceConfidentialTransferFrom(address,address,bytes32)"))
279
+ selector == 0x6c9c3c85 || // bytes4(keccak256("forceConfidentialTransferFrom(address,address,bytes32,bytes)"))
280
+ selector == 0x44fd6e40 || // bytes4(keccak256("forceConfidentialTransferFrom(address,address,bytes32)"))
281
+ selector == this.recoverAddress.selector;
245
282
  }
246
283
  }
@@ -0,0 +1,92 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Confidential Contracts (last updated v0.5.0-rc.0) (token/ERC7984/utils/ERC7984BalanceCapHookModule.sol)
3
+
4
+ pragma solidity ^0.8.27;
5
+
6
+ import {FHE, ebool, euint64, externalEuint64} from "@fhevm/solidity/lib/FHE.sol";
7
+ import {IERC7984Rwa} from "./../../../interfaces/IERC7984Rwa.sol";
8
+ import {FHESafeMath} from "./../../../utils/FHESafeMath.sol";
9
+ import {ERC7984HookModule} from "./ERC7984HookModule.sol";
10
+
11
+ /**
12
+ * @dev An ERC-7984 hook module that limits the balance of each investor.
13
+ *
14
+ * The cap is stored as an encrypted `euint64` value. The pre-transfer hook compares the recipient's prospective balance to the
15
+ * encrypted cap and emits an encrypted compliance result via {ERC7984HookModule-_emitPreTransferResults}.
16
+ *
17
+ * This module is compatible with {ERC7984Hooked}.
18
+ *
19
+ * WARNING: This module notifies senders of the result of the pre-transfer hook. This can be used to leak
20
+ * information about the balance of the recipient. This is a potential security risk and should be used
21
+ * with caution. Production use-cases may want to remove this notification.
22
+ */
23
+ contract ERC7984BalanceCapHookModule is ERC7984HookModule {
24
+ /// @dev Emitted when the max balance for a given token is set.
25
+ event ERC7984BalanceCapHookModuleMaxBalanceSet(address indexed token, euint64 newMaxBalance);
26
+
27
+ mapping(address => euint64) private _maxBalances;
28
+
29
+ /**
30
+ * @dev Sets the max balance for a given token `token` to the encrypted value `newMaxBalance`.
31
+ *
32
+ * `msg.sender` must have the agent role on `token`.
33
+ */
34
+ function setMaxBalance(address token, externalEuint64 newMaxBalance, bytes calldata inputProof) public virtual {
35
+ require(IERC7984Rwa(token).isAgent(msg.sender), ERC7984HookModuleUnauthorizedAccount(msg.sender));
36
+ _setMaxBalance(token, FHE.fromExternal(newMaxBalance, inputProof));
37
+ }
38
+
39
+ /// @dev Gets the encrypted max balance for a given token `token`. Returns the zero handle if unset.
40
+ function maxBalance(address token) public view virtual returns (euint64) {
41
+ return _maxBalances[token];
42
+ }
43
+
44
+ /// @dev Sets the encrypted max balance for a given token, grants the module persistent ACL, and emits an event.
45
+ function _setMaxBalance(address token, euint64 newMaxBalance) internal virtual {
46
+ _maxBalances[token] = newMaxBalance;
47
+ FHE.allowThis(newMaxBalance);
48
+ FHE.allow(newMaxBalance, msg.sender);
49
+
50
+ emit ERC7984BalanceCapHookModuleMaxBalanceSet(token, newMaxBalance);
51
+ }
52
+
53
+ /// @inheritdoc ERC7984HookModule
54
+ function _preTransfer(
55
+ address token,
56
+ address from,
57
+ address to,
58
+ euint64 encryptedAmount
59
+ ) internal override returns (ebool) {
60
+ ebool compliant;
61
+ if (to == address(0) || from == to || !FHE.isInitialized(maxBalance(token))) {
62
+ compliant = FHE.asEbool(true);
63
+ } else {
64
+ euint64 balance = IERC7984Rwa(token).confidentialBalanceOf(to);
65
+ _accessHandle(token, balance);
66
+
67
+ // Note, if the balance would result in an overflow, transfer will fail due to total supply overflow.
68
+ (, euint64 futureBalance) = FHESafeMath.tryIncrease(balance, encryptedAmount);
69
+ compliant = FHE.le(futureBalance, maxBalance(token));
70
+ }
71
+
72
+ _emitPreTransferResults(token, from, to, encryptedAmount, compliant, bytes32(0));
73
+
74
+ return FHE.and(compliant, super._preTransfer(token, from, to, encryptedAmount));
75
+ }
76
+
77
+ /**
78
+ * @dev See {ERC7984HookModule-_onInstall}. The `initData` must contain the initial max balance for the token
79
+ * along with the input proof for the max balance. These are encoded using standard ABI encoding.
80
+ */
81
+ function _onInstall(address token, bytes calldata initData) internal virtual override {
82
+ super._onInstall(token, initData);
83
+ (externalEuint64 maxBalance_, bytes memory inputProof) = abi.decode(initData, (externalEuint64, bytes));
84
+ _setMaxBalance(token, FHE.fromExternal(maxBalance_, inputProof));
85
+ }
86
+
87
+ /// @inheritdoc ERC7984HookModule
88
+ function _onUninstall(address token, bytes calldata deinitData) internal virtual override {
89
+ super._onUninstall(token, deinitData);
90
+ _maxBalances[token] = euint64.wrap(0);
91
+ }
92
+ }
@@ -0,0 +1,145 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // OpenZeppelin Confidential Contracts (last updated v0.5.0-rc.0) (token/ERC7984/utils/ERC7984HolderCapHookModule.sol)
3
+
4
+ pragma solidity ^0.8.27;
5
+
6
+ import {FHE, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol";
7
+ import {IERC7984Rwa} from "./../../../interfaces/IERC7984Rwa.sol";
8
+ import {ERC7984HookModule} from "./ERC7984HookModule.sol";
9
+
10
+ /**
11
+ * @dev An ERC-7984 hook module that limits the number of holders for a given token.
12
+ *
13
+ * NOTE: This module must be installed prior to minting any tokens. After the total supply is initialized,
14
+ * it is not possible to guarantee that the number of holders is 0, so the module can not be installed.
15
+ *
16
+ * WARNING: This module may not function correctly with non-standard tokens such as fee on transfer.
17
+ */
18
+ contract ERC7984HolderCapHookModule is ERC7984HookModule {
19
+ /// @dev Emitted when the max holder count for a given token is set.
20
+ event ERC7984HolderCapHookModuleMaxHolderCountSet(address indexed token, uint64 newMaxHolderCount);
21
+
22
+ /// @dev The new max holder count `maxHolderCount` is invalid.
23
+ error ERC7984HolderCapHookModuleInvalidMaxHolderCount(uint64 maxHolderCount);
24
+
25
+ /**
26
+ * The total supply of the token is already initialized.
27
+ * This module must be installed before the total supply is initialized.
28
+ */
29
+ error ERC7984HolderCapHookModuleTotalSupplyInitialized();
30
+
31
+ mapping(address => uint64) private _maxHolderCounts;
32
+ mapping(address => euint64) private _holderCounts;
33
+
34
+ /**
35
+ * @dev Sets the max number of holders for the given token `token` to `maxHolderCount_`.
36
+ *
37
+ * `msg.sender` must have the agent role on `token`
38
+ **/
39
+ function setMaxHolderCount(address token, uint64 maxHolderCount_) public virtual {
40
+ require(IERC7984Rwa(token).isAgent(msg.sender), ERC7984HookModuleUnauthorizedAccount(msg.sender));
41
+ _setMaxHolderCount(token, maxHolderCount_);
42
+ }
43
+
44
+ /// @dev Gets max number of holders for the given token `token`.
45
+ function maxHolderCount(address token) public view virtual returns (uint64) {
46
+ return _maxHolderCounts[token];
47
+ }
48
+
49
+ /// @dev Gets current number of holders for the given token `token`.
50
+ function holderCount(address token) public view virtual returns (euint64) {
51
+ return _holderCounts[token];
52
+ }
53
+
54
+ /// @dev Sets the max holder count for a given token to `maxHolderCount_` and emits an event.
55
+ function _setMaxHolderCount(address token, uint64 maxHolderCount_) internal virtual {
56
+ require(maxHolderCount_ != 0, ERC7984HolderCapHookModuleInvalidMaxHolderCount(maxHolderCount_));
57
+ _maxHolderCounts[token] = maxHolderCount_;
58
+ emit ERC7984HolderCapHookModuleMaxHolderCountSet(token, maxHolderCount_);
59
+ }
60
+
61
+ /// @inheritdoc ERC7984HookModule
62
+ function _preTransfer(
63
+ address token,
64
+ address from,
65
+ address to,
66
+ euint64 encryptedAmount
67
+ ) internal override returns (ebool) {
68
+ if (to == address(0) || to == from) {
69
+ return FHE.asEbool(true);
70
+ }
71
+
72
+ euint64 fromBalance = IERC7984Rwa(token).confidentialBalanceOf(from);
73
+ euint64 toBalance = IERC7984Rwa(token).confidentialBalanceOf(to);
74
+
75
+ _accessHandle(token, fromBalance);
76
+ _accessHandle(token, toBalance);
77
+
78
+ euint64 encryptedZero = FHE.asEuint64(0);
79
+
80
+ // note, if from is address(0):
81
+ // - fromBalance is an encrypted zero
82
+ // - from will be (erroneously) removed from the holder count only encryptedAmount is a zero
83
+ // that is fine because if encryptedAmount is a zero, then this value is dropped anyway.
84
+ euint64 adjustedHolderCount = FHE.add(
85
+ FHE.sub(holderCount(token), FHE.asEuint64(FHE.eq(fromBalance, encryptedAmount))),
86
+ FHE.asEuint64(FHE.and(FHE.eq(toBalance, encryptedZero), FHE.ne(encryptedAmount, encryptedZero)))
87
+ );
88
+
89
+ ebool compliant = FHE.le(adjustedHolderCount, maxHolderCount(token));
90
+
91
+ return FHE.and(compliant, super._preTransfer(token, from, to, encryptedAmount));
92
+ }
93
+
94
+ /// @inheritdoc ERC7984HookModule
95
+ function _postTransfer(address token, address from, address to, euint64 encryptedAmount) internal virtual override {
96
+ super._postTransfer(token, from, to, encryptedAmount);
97
+
98
+ if (from == to) return;
99
+
100
+ euint64 fromBalance = IERC7984Rwa(token).confidentialBalanceOf(from);
101
+ euint64 toBalance = IERC7984Rwa(token).confidentialBalanceOf(to);
102
+
103
+ _accessHandle(token, fromBalance);
104
+ _accessHandle(token, toBalance);
105
+
106
+ euint64 encryptedZero = FHE.asEuint64(0);
107
+ ebool transferNotZero = FHE.ne(encryptedAmount, encryptedZero);
108
+ euint64 newHolderCount = holderCount(token);
109
+
110
+ if (to != address(0)) {
111
+ ebool addHolder = FHE.and(transferNotZero, FHE.eq(toBalance, encryptedAmount));
112
+ newHolderCount = FHE.add(newHolderCount, FHE.asEuint64(addHolder));
113
+ }
114
+
115
+ if (from != address(0)) {
116
+ ebool subHolder = FHE.and(transferNotZero, FHE.eq(fromBalance, encryptedZero));
117
+ newHolderCount = FHE.sub(newHolderCount, FHE.asEuint64(subHolder));
118
+ }
119
+
120
+ _holderCounts[token] = newHolderCount;
121
+ FHE.allowThis(newHolderCount);
122
+ }
123
+
124
+ /**
125
+ * @dev See {ERC7984HookModule-_onInstall}. The `initData` must contain the initial max holder count for the token
126
+ * as a standard ABI encoded uint64.
127
+ **/
128
+ function _onInstall(address token, bytes calldata initData) internal virtual override {
129
+ require(
130
+ !FHE.isInitialized(IERC7984Rwa(token).confidentialTotalSupply()),
131
+ ERC7984HolderCapHookModuleTotalSupplyInitialized()
132
+ );
133
+
134
+ super._onInstall(token, initData);
135
+
136
+ uint64 maxHolderCount_ = abi.decode(initData, (uint64));
137
+ _setMaxHolderCount(token, maxHolderCount_);
138
+ }
139
+
140
+ function _onUninstall(address token, bytes calldata deinitData) internal virtual override {
141
+ super._onUninstall(token, deinitData);
142
+ delete _maxHolderCounts[token];
143
+ _holderCounts[token] = euint64.wrap(0);
144
+ }
145
+ }