11// SPDX-License-Identifier: UNLICENSED
22pragma solidity 0.8.19 ;
33
4+ import {Context} from "openzeppelin/contracts/utils/Context.sol " ;
5+ import {ECDSA} from "openzeppelin/contracts/utils/cryptography/ECDSA.sol " ;
6+ import {EIP712} from "openzeppelin/contracts/utils/cryptography/EIP712.sol " ;
7+ import {Nonces} from "openzeppelin-latest/contracts/utils/Nonces.sol " ;
48import {Ownable2Step} from "openzeppelin/contracts/access/Ownable2Step.sol " ;
5- import {ERC2771Context } from "openzeppelin-contracts/contracts/metatx/ERC2771Context.sol " ;
69import {Pausable} from "openzeppelin/contracts/security/Pausable.sol " ;
7- import {Context} from "openzeppelin/contracts/utils/Context.sol " ;
8-
910/**
1011 * @title IdRegistry
1112 * @author @v
@@ -19,7 +20,8 @@ import {Context} from "openzeppelin/contracts/utils/Context.sol";
1920 * Registry implements a recovery system which lets the address that owns an fid nominate
2021 * a recovery address that can transfer the fid to a new address.
2122 */
22- contract IdRegistry is ERC2771Context , Ownable2Step , Pausable {
23+
24+ contract IdRegistry is Ownable2Step , Pausable , EIP712 , Nonces {
2325 /*//////////////////////////////////////////////////////////////
2426 ERRORS
2527 //////////////////////////////////////////////////////////////*/
@@ -42,6 +44,12 @@ contract IdRegistry is ERC2771Context, Ownable2Step, Pausable {
4244 /// @dev Revert when an invalid address is provided as input.
4345 error InvalidAddress ();
4446
47+ /// @dev Revert when the signature provided is invalid.
48+ error InvalidSignature ();
49+
50+ /// @dev Revert when the block.timestamp is ahead of the signature deadline.
51+ error SignatureExpired ();
52+
4553 /*//////////////////////////////////////////////////////////////
4654 EVENTS
4755 //////////////////////////////////////////////////////////////*/
@@ -84,6 +92,16 @@ contract IdRegistry is ERC2771Context, Ownable2Step, Pausable {
8492 */
8593 event DisableTrustedOnly ();
8694
95+ /*//////////////////////////////////////////////////////////////
96+ CONSTANTS
97+ //////////////////////////////////////////////////////////////*/
98+
99+ bytes32 internal constant _REGISTER_TYPEHASH =
100+ keccak256 ("Register(address to,address recovery,uint256 nonce,uint256 deadline) " );
101+
102+ bytes32 internal constant _TRANSFER_TYPEHASH =
103+ keccak256 ("Transfer(address from,address to,uint256 nonce,uint256 deadline) " );
104+
87105 /*//////////////////////////////////////////////////////////////
88106 PARAMETERS
89107 //////////////////////////////////////////////////////////////*/
@@ -123,44 +141,55 @@ contract IdRegistry is ERC2771Context, Ownable2Step, Pausable {
123141 //////////////////////////////////////////////////////////////*/
124142
125143 /**
126- * @notice Set the owner of the contract to the deployer and configure the trusted forwarder.
127- *
128- * @param _forwarder The address of the ERC2771 forwarder contract that this contract trusts to
129- * verify the authenticity of signed meta-transaction requests.
144+ * @notice Set the owner of the contract to the deployer.
130145 */
131146 // solhint-disable-next-line no-empty-blocks
132- constructor (address _forwarder ) ERC2771Context (_forwarder ) {}
147+ constructor () EIP712 ( " Farcaster IdRegistry " , " 1 " ) {}
133148
134149 /*//////////////////////////////////////////////////////////////
135150 REGISTRATION LOGIC
136151 //////////////////////////////////////////////////////////////*/
137152
138153 /**
139- * @notice Register a new Farcaster ID (fid) to an address . The address must not have an fid
140- * and the contract must not be in the Registrable (trustedOnly = 0) state.
154+ * @notice Register a new Farcaster ID (fid) to the caller . The caller must not have an fid.
155+ * Rthe contract must not be in the Registrable (trustedOnly = 0) state.
141156 *
142- * @param to Address which will own the fid
143157 * @param recovery Address which can recover the fid. Set to zero to disable recovery.
144158 */
145- function register (address to , address recovery ) external returns (uint256 fid ) {
146- if (trustedOnly == 1 ) revert Seedable ();
147-
148- fid = _unsafeRegister (to, recovery);
159+ function register (address recovery ) external returns (uint256 fid ) {
160+ return _register (msg .sender , recovery);
161+ }
149162
150- emit Register (to, idCounter, recovery);
163+ /**
164+ * @notice Register a new Farcaster ID (fid) to any address. A signed message from the address
165+ * must be provided. The address must not have an fid. The contract must be in the
166+ * Registrable (trustedOnly = 0) state.
167+ *
168+ * @param to Address which will own the fid.
169+ * @param recovery Address which can recover the fid. Set to zero to disable recovery.
170+ * @param deadline Expiration timestamp of the signature.
171+ * @param sig EIP-712 signature signed by the to address.
172+ */
173+ function registerFor (
174+ address to ,
175+ address recovery ,
176+ uint256 deadline ,
177+ bytes calldata sig
178+ ) external returns (uint256 fid ) {
179+ _verifyRegisterSig (to, recovery, deadline, sig);
180+ return _register (to, recovery);
151181 }
152182
153183 /**
154- * @notice Register a new unique Farcaster ID (fid) for an address that does not have one. This
155- * can only be invoked by the trusted caller when trustedOnly is set to 1 .
184+ * @notice Register a new Farcaster ID (fid) to any address. The address must not have an fid.
185+ * The contract must be in the Seedable ( trustedOnly = 1) state .
156186 *
157- * @param to The address which will control the fid
158- * @param recovery The address which can recover the fid
187+ * @param to The address which will own the fid.
188+ * @param recovery The address which can recover the fid.
159189 */
160190 function trustedRegister (address to , address recovery ) external returns (uint256 fid ) {
161191 if (trustedOnly == 0 ) revert Registrable ();
162192
163- /* Perf: Save 100 gas using msg.sender over msgSender() since meta-tx aren't needed. */
164193 if (msg .sender != trustedCaller) revert Unauthorized ();
165194
166195 fid = _unsafeRegister (to, recovery);
@@ -169,16 +198,29 @@ contract IdRegistry is ERC2771Context, Ownable2Step, Pausable {
169198 }
170199
171200 /**
172- * @dev Registers a new, unique fid and sets up a recovery address for a caller without
173- * checking all invariants or emitting events.
201+ * @dev Registers an fid and sets up a recovery address for a target. The contract must not be
202+ * in the Seedable (trustedOnly = 1) state and the target must not have an fid.
203+ */
204+ function _register (address to , address recovery ) internal returns (uint256 fid ) {
205+ if (trustedOnly == 1 ) revert Seedable ();
206+
207+ fid = _unsafeRegister (to, recovery);
208+
209+ emit Register (to, idCounter, recovery);
210+ }
211+
212+ /**
213+ * @dev Registers an fid and sets up a recovery address for a target. Does not check all
214+ * invariants or emit events. The contract must not be in the Seedable (trustedOnly = 1)
215+ * state and the target must not have an fid.
174216 */
175217 function _unsafeRegister (address to , address recovery ) internal whenNotPaused returns (uint256 fid ) {
176- /* Revert if the destination (to) already has an fid */
218+ /* Revert if the target (to) has an fid */
177219 if (idOf[to] != 0 ) revert HasId ();
178220
179221 /* Safety: idCounter won't realistically overflow. */
180- /* Incrementing before assignment ensures that no one gets the 0 fid. */
181222 unchecked {
223+ /* Incrementing before assignment ensures that no one gets the 0 fid. */
182224 fid = ++ idCounter;
183225 }
184226
@@ -193,18 +235,23 @@ contract IdRegistry is ERC2771Context, Ownable2Step, Pausable {
193235
194236 /**
195237 * @notice Transfer the fid owned by this address to another address that does not have an fid.
196- * Supports ERC 2771 meta-transactions and can be called via a relayer.
238+ * Supports ERC 2771 meta-transactions and can be called via a relayer. A signed message
239+ * from the destination address must be provided.
197240 *
198241 * @param to The address to transfer the fid to.
242+ * @param deadline Expiration timestamp of the signature.
243+ * @param sig EIP-712 signature signed by the to address.
199244 */
200- function transfer (address to ) external {
201- address from = _msgSender () ;
245+ function transfer (address to , uint256 deadline , bytes calldata sig ) external {
246+ address from = msg . sender ;
202247 uint256 fromId = idOf[from];
203248
204249 /* Revert if the sender has no id or receipient has an id */
205250 if (fromId == 0 ) revert HasNoId ();
206251 if (idOf[to] != 0 ) revert HasId ();
207252
253+ _verifyTransferSig (fromId, to, deadline, sig);
254+
208255 _unsafeTransfer (fromId, from, to);
209256 }
210257
@@ -239,7 +286,7 @@ contract IdRegistry is ERC2771Context, Ownable2Step, Pausable {
239286 */
240287 function changeRecoveryAddress (address recovery ) external {
241288 /* Revert if the caller does not own an fid */
242- uint256 ownerId = idOf[_msgSender () ];
289+ uint256 ownerId = idOf[msg . sender ];
243290 if (ownerId == 0 ) revert HasNoId ();
244291
245292 /* Change the recovery address */
@@ -248,20 +295,32 @@ contract IdRegistry is ERC2771Context, Ownable2Step, Pausable {
248295 emit ChangeRecoveryAddress (ownerId, recovery);
249296 }
250297
251- function recover (address from , address to ) external {
298+ /**
299+ * @notice Transfer the fid from the from address to the to address. Must be called by the
300+ * recovery address. Supports ERC 2771 meta-transactions and can be called via a
301+ * relayer. A signed message from the to address must be provided.
302+ *
303+ * @param from The address that currently owns the fid.
304+ * @param to The address to transfer the fid to.
305+ * @param deadline Expiration timestamp of the signature.
306+ * @param sig EIP-712 signature signed by the to address.
307+ */
308+ function recover (address from , address to , uint256 deadline , bytes calldata sig ) external {
252309 /* Revert if from does not own an fid */
253- uint256 ownerId = idOf[from];
254- if (ownerId == 0 ) revert HasNoId ();
310+ uint256 fromId = idOf[from];
311+ if (fromId == 0 ) revert HasNoId ();
255312
256313 /* Revert if the caller is not the recovery address */
257- address caller = _msgSender () ;
258- address recoveryAddress = recoveryOf[ownerId ];
314+ address caller = msg . sender ;
315+ address recoveryAddress = recoveryOf[fromId ];
259316 if (recoveryAddress != caller) revert Unauthorized ();
260317
261318 /* Revert if destination(to) already has an fid */
262319 if (idOf[to] != 0 ) revert HasId ();
263320
264- _unsafeTransfer (ownerId, from, to);
321+ _verifyTransferSig (fromId, to, deadline, sig);
322+
323+ _unsafeTransfer (fromId, from, to);
265324 }
266325
267326 /*//////////////////////////////////////////////////////////////
@@ -304,14 +363,23 @@ contract IdRegistry is ERC2771Context, Ownable2Step, Pausable {
304363 }
305364
306365 /*//////////////////////////////////////////////////////////////
307- OPEN ZEPPELIN OVERRIDES
366+ SIGNATURE VERIFICATION HELPERS
308367 //////////////////////////////////////////////////////////////*/
309368
310- function _msgSender () internal view override (Context, ERC2771Context ) returns (address ) {
311- return ERC2771Context ._msgSender ();
369+ function _verifyRegisterSig (address to , address recovery , uint256 deadline , bytes memory sig ) internal {
370+ if (block .timestamp >= deadline) revert SignatureExpired ();
371+ bytes32 digest =
372+ _hashTypedDataV4 (keccak256 (abi.encode (_REGISTER_TYPEHASH, to, recovery, _useNonce (to), deadline)));
373+
374+ address recovered = ECDSA.recover (digest, sig);
375+ if (recovered != to) revert InvalidSignature ();
312376 }
313377
314- function _msgData () internal view override (Context, ERC2771Context ) returns (bytes calldata ) {
315- return ERC2771Context ._msgData ();
378+ function _verifyTransferSig (uint256 fid , address to , uint256 deadline , bytes memory sig ) internal {
379+ if (block .timestamp >= deadline) revert SignatureExpired ();
380+ bytes32 digest = _hashTypedDataV4 (keccak256 (abi.encode (_TRANSFER_TYPEHASH, fid, to, _useNonce (to), deadline)));
381+
382+ address recovered = ECDSA.recover (digest, sig);
383+ if (recovered != to) revert InvalidSignature ();
316384 }
317385}
0 commit comments