@@ -9,11 +9,47 @@ import {IERC734KeyHasPurpose, IIdentityRegistryERC3643} from "../registry/interf
99 * compute an ERC-165 interface ID and is explicitly not meant to be used as a type.
1010 */
1111interface IERC3643ComplianceForToken {
12+ /**
13+ * @notice Binds a token to this compliance contract.
14+ * @param token The token being bound.
15+ */
1216 function bindToken (address token ) external ;
17+
18+ /**
19+ * @notice Unbinds a previously bound token.
20+ * @param token The token being unbound.
21+ */
1322 function unbindToken (address token ) external ;
23+
24+ /**
25+ * @notice Notifies the compliance contract that a transfer has occurred.
26+ * @param from The sender.
27+ * @param to The recipient.
28+ * @param value The amount moved.
29+ */
1430 function transferred (address from , address to , uint256 value ) external ;
31+
32+ /**
33+ * @notice Notifies the compliance contract that tokens have been minted.
34+ * @param to The recipient.
35+ * @param value The amount minted.
36+ */
1537 function created (address to , uint256 value ) external ;
38+
39+ /**
40+ * @notice Notifies the compliance contract that tokens have been burned.
41+ * @param from The holder burned from.
42+ * @param value The amount burned.
43+ */
1644 function destroyed (address from , uint256 value ) external ;
45+
46+ /**
47+ * @notice Returns whether a transfer is allowed.
48+ * @param from The sender, or the zero address for a mint.
49+ * @param to The recipient.
50+ * @param value The amount to move.
51+ * @return True when the transfer is allowed.
52+ */
1753 function canTransfer (address from , address to , uint256 value ) external view returns (bool );
1854}
1955
@@ -42,6 +78,9 @@ interface IERC3643ComplianceForToken {
4278 * the cross-check circular.
4379 */
4480contract ERC3643TokenMock {
81+ /**
82+ * @notice The identity registry this token consults on every inbound transfer.
83+ */
4584 IIdentityRegistryERC3643 public identityRegistry;
4685 /**
4786 * @notice The compliance contract, i.e. a `RuleEngine`.
@@ -51,11 +90,32 @@ contract ERC3643TokenMock {
5190 */
5291 IERC3643ComplianceForToken public compliance;
5392
93+ /**
94+ * @notice Token balance per account.
95+ */
5496 mapping (address account = > uint256 balance ) public balanceOf;
97+ /**
98+ * @notice Whether an account holds the ERC-3643 agent role.
99+ */
55100 mapping (address account = > bool isAgent ) public isAgent;
101+ /**
102+ * @notice Total token supply.
103+ */
56104 uint256 public totalSupply;
57105
106+ /**
107+ * @notice Emitted on every balance movement, including mint and burn.
108+ * @param from The sender, or the zero address for a mint.
109+ * @param to The recipient, or the zero address for a burn.
110+ * @param value The amount moved.
111+ */
58112 event Transfer (address indexed from , address indexed to , uint256 value );
113+ /**
114+ * @notice Emitted when a position is recovered onto a replacement wallet.
115+ * @param lostWallet The wallet recovered from.
116+ * @param newWallet The replacement wallet.
117+ * @param investorOnchainId The identity contract that vouched for the replacement wallet.
118+ */
59119 event RecoverySuccess (address indexed lostWallet , address indexed newWallet , address indexed investorOnchainId );
60120
61121 error ERC3643TokenMock_OnlyAgent ();
@@ -147,27 +207,6 @@ contract ERC3643TokenMock {
147207 revert ("Transfer not possible " );
148208 }
149209
150- /**
151- * @notice Agent-forced transfer; the recipient must still be verified.
152- * @dev `Token.forcedTransfer`: bypasses freezes but NOT the registry check on `_to`.
153- * @param _from Sender.
154- * @param _to Recipient.
155- * @param _amount Amount to transfer.
156- * @return True on success.
157- */
158- function forcedTransfer (address _from , address _to , uint256 _amount ) public onlyAgent returns (bool ) {
159- require (balanceOf[_from] >= _amount, "sender balance too low " );
160- // NOTE: `Token.forcedTransfer` does NOT consult `canTransfer` -- it only notifies
161- // `transferred` afterwards. A compliance contract that reverts in `transferred` (as a
162- // RuleEngine does) still blocks the move; one that only answers `canTransfer` does not.
163- if (identityRegistry.isVerified (_to)) {
164- _transfer (_from, _to, _amount);
165- _complianceTransferred (_from, _to, _amount);
166- return true ;
167- }
168- revert ("Transfer not possible " );
169- }
170-
171210 /**
172211 * @notice Mints tokens; the recipient must be verified.
173212 * @dev `Token.mint`: `require(isVerified(_to), "Identity is not verified.")`.
@@ -205,56 +244,62 @@ contract ERC3643TokenMock {
205244 * @notice Moves an investor's position to a replacement wallet.
206245 * @dev Transcribed from `Token.recoveryAddress`, preserving the order that matters:
207246 * 1. `keyHasPurpose(keccak256(abi.encode(_newWallet)), 1)` on the **caller-supplied**
208- * `_investorOnchainID ` -- reverts "Recovery not possible" when false;
247+ * `_investorOnchainId ` -- reverts "Recovery not possible" when false;
209248 * 2. `investorCountry(_lostWallet)` read from the registry;
210- * 3. `registerIdentity(_newWallet, _investorOnchainID , country)` -- called BY THE TOKEN;
249+ * 3. `registerIdentity(_newWallet, _investorOnchainId , country)` -- called BY THE TOKEN;
211250 * 4. `forcedTransfer(_lostWallet, _newWallet, balance)`;
212251 * 5. `deleteIdentity(_lostWallet)` -- also called BY THE TOKEN.
213252 * @param _lostWallet The wallet to recover from.
214253 * @param _newWallet The replacement wallet.
215- * @param _investorOnchainID The contract answering `keyHasPurpose`.
254+ * @param _investorOnchainId The contract answering `keyHasPurpose`.
216255 * @return True on success.
217256 */
218- // forge-lint: disable-next-line(mixed-case-variable)
219- function recoveryAddress (address _lostWallet , address _newWallet , address _investorOnchainID )
257+ function recoveryAddress (address _lostWallet , address _newWallet , address _investorOnchainId )
220258 external
221259 onlyAgent
222260 returns (bool )
223261 {
224262 require (balanceOf[_lostWallet] != 0 , "no tokens to recover " );
225263 // forge-lint: disable-next-line(asm-keccak256)
226264 bytes32 _key = keccak256 (abi.encode (_newWallet));
227- if (IERC734KeyHasPurpose (_investorOnchainID ).keyHasPurpose (_key, 1 )) {
265+ if (IERC734KeyHasPurpose (_investorOnchainId ).keyHasPurpose (_key, 1 )) {
228266 uint256 investorTokens = balanceOf[_lostWallet];
229267 identityRegistry.registerIdentity (
230- _newWallet, _investorOnchainID , identityRegistry.investorCountry (_lostWallet)
268+ _newWallet, _investorOnchainId , identityRegistry.investorCountry (_lostWallet)
231269 );
232270 forcedTransfer (_lostWallet, _newWallet, investorTokens);
233271 identityRegistry.deleteIdentity (_lostWallet);
234- emit RecoverySuccess (_lostWallet, _newWallet, _investorOnchainID );
272+ emit RecoverySuccess (_lostWallet, _newWallet, _investorOnchainId );
235273 return true ;
236274 }
237275 revert ("Recovery not possible " );
238276 }
239277
240- /*//////////////////////////////////////////////////////////////
241- INTERNAL FUNCTIONS
242- //////////////////////////////////////////////////////////////*/
243-
244278 /**
245- * @notice Asks the compliance contract whether a move is allowed; true when none is set.
246- * @param from Sender, or the zero address for a mint.
247- * @param to Recipient.
248- * @param amount Amount to move.
249- * @return True when the move is allowed.
279+ * @notice Agent-forced transfer; the recipient must still be verified.
280+ * @dev `Token.forcedTransfer`: bypasses freezes but NOT the registry check on `_to`.
281+ * @param _from Sender.
282+ * @param _to Recipient.
283+ * @param _amount Amount to transfer.
284+ * @return True on success.
250285 */
251- function _canTransfer (address from , address to , uint256 amount ) internal view returns (bool ) {
252- if (address (compliance) == address (0 )) {
286+ function forcedTransfer (address _from , address _to , uint256 _amount ) public onlyAgent returns (bool ) {
287+ require (balanceOf[_from] >= _amount, "sender balance too low " );
288+ // NOTE: `Token.forcedTransfer` does NOT consult `canTransfer` -- it only notifies
289+ // `transferred` afterwards. A compliance contract that reverts in `transferred` (as a
290+ // RuleEngine does) still blocks the move; one that only answers `canTransfer` does not.
291+ if (identityRegistry.isVerified (_to)) {
292+ _transfer (_from, _to, _amount);
293+ _complianceTransferred (_from, _to, _amount);
253294 return true ;
254295 }
255- return compliance. canTransfer (from, to, amount );
296+ revert ( " Transfer not possible " );
256297 }
257298
299+ /*//////////////////////////////////////////////////////////////
300+ INTERNAL FUNCTIONS
301+ //////////////////////////////////////////////////////////////*/
302+
258303 /**
259304 * @notice Notifies the compliance contract that a transfer happened; no-op when none is set.
260305 * @param from Sender.
@@ -278,4 +323,18 @@ contract ERC3643TokenMock {
278323 balanceOf[to] += amount;
279324 emit Transfer (from, to, amount);
280325 }
326+
327+ /**
328+ * @notice Asks the compliance contract whether a move is allowed; true when none is set.
329+ * @param from Sender, or the zero address for a mint.
330+ * @param to Recipient.
331+ * @param amount Amount to move.
332+ * @return True when the move is allowed.
333+ */
334+ function _canTransfer (address from , address to , uint256 amount ) internal view returns (bool ) {
335+ if (address (compliance) == address (0 )) {
336+ return true ;
337+ }
338+ return compliance.canTransfer (from, to, amount);
339+ }
281340}
0 commit comments