Skip to content

Commit 8a0e4bb

Browse files
feat(IdRegistry): require sigs when registering, transferring or recovering to another address (#261)
* feat(IdRegistry): register with signature * refactor: register from registrar requires 1 param, add tests, rename for clarity * feat(IdRegistry): transfer with signature * feat(IdRegistry): recovery with signature * refactor(IdRegistry): rename register with signature to registerFor * refactor(IdRegistry): update function docs, re-arrange functions for clarity * test(IdRegistry): add coverage for register * feat(IdRegistry): remove meta-txns with ERC2771 * refactor: remove all references to forwarder * test(IdRegistry): add prechecks to tests * test(IdRegistry): add coverage for InvalidSignature case --------- Co-authored-by: Varun Srinivasan <[email protected]>
1 parent 0061a11 commit 8a0e4bb

17 files changed

+826
-330
lines changed

.env.example

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,4 @@ STORAGE_RENT_VAULT_ADDRESS=
44
STORAGE_RENT_ADMIN_ADDRESS=
55
STORAGE_RENT_OPERATOR_ADDRESS=
66
STORAGE_RENT_TREASURER_ADDRESS=
7-
ID_REGISTRY_TRUSTED_FORWARDER_ADDRESS=
87
ID_REGISTRY_OWNER_ADDRESS=

.gas-snapshot

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
BundleRegistryGasUsageTest:testGasRegister() (gas: 575844)
2-
BundleRegistryGasUsageTest:testGasTrustedBatchRegister() (gas: 3491830)
3-
BundleRegistryGasUsageTest:testGasTrustedRegister() (gas: 506862)
4-
IdRegistryGasUsageTest:testGasRegisterAndRecover() (gas: 933249)
5-
IdRegistryGasUsageTest:testGasRegisterFromTrustedCaller() (gas: 804430)
1+
BundleRegistryGasUsageTest:testGasRegisterWithSig() (gas: 815167)
2+
BundleRegistryGasUsageTest:testGasTrustedBatchRegister() (gas: 3491213)
3+
BundleRegistryGasUsageTest:testGasTrustedRegister() (gas: 506695)
4+
IdRegistryGasUsageTest:testGasRegister() (gas: 732876)
5+
IdRegistryGasUsageTest:testGasRegisterForAndRecover() (gas: 1696472)
6+
IdRegistryGasUsageTest:testGasRegisterFromTrustedCaller() (gas: 804513)
67
StorageRentGasUsageTest:testGasBatchCredit() (gas: 169333)
78
StorageRentGasUsageTest:testGasBatchRent() (gas: 267476)
89
StorageRentGasUsageTest:testGasContinuousCredit() (gas: 142400)

script/Deploy.s.sol

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ contract Deploy is Script {
2222
function run() public {
2323
_etchCreate2Deployer();
2424

25-
address trustedForwarder = vm.envAddress("ID_REGISTRY_TRUSTED_FORWARDER_ADDRESS");
2625
address initialOwner = vm.envAddress("ID_REGISTRY_OWNER_ADDRESS");
2726

2827
address vault = vm.envAddress("STORAGE_RENT_VAULT_ADDRESS");
@@ -33,7 +32,7 @@ contract Deploy is Script {
3332

3433
vm.startBroadcast();
3534
(AggregatorV3Interface priceFeed, AggregatorV3Interface uptimeFeed) = _getOrDeployPriceFeeds();
36-
IdRegistryFab idRegistry = new IdRegistryFab(trustedForwarder, initialOwner, ID_REGISTRY_CREATE2_SALT);
35+
IdRegistryFab idRegistry = new IdRegistryFab(initialOwner, ID_REGISTRY_CREATE2_SALT);
3736
StorageRent storageRent = new StorageRent{ salt: STORAGE_RENT_CREATE2_SALT }(
3837
priceFeed,
3938
uptimeFeed,

script/IdRegistry.s.sol

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ contract IdRegistryScript is Script {
88
bytes32 internal constant CREATE2_SALT = "fc";
99

1010
function run() public {
11-
address trustedForwarder = vm.envAddress("ID_REGISTRY_TRUSTED_FORWARDER_ADDRESS");
1211
address initialOwner = vm.envAddress("ID_REGISTRY_OWNER_ADDRESS");
1312

1413
vm.broadcast();
15-
new IdRegistryFab(trustedForwarder, initialOwner, CREATE2_SALT);
14+
new IdRegistryFab(initialOwner, CREATE2_SALT);
1615
}
1716
}

script/helpers/IdRegistryFab.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {IdRegistry} from "../../src/IdRegistry.sol";
1111
contract IdRegistryFab {
1212
address public immutable registryAddr;
1313

14-
constructor(address trustedForwarder, address initialOwner, bytes32 salt) {
15-
IdRegistry registry = new IdRegistry{ salt: salt }(trustedForwarder);
14+
constructor(address initialOwner, bytes32 salt) {
15+
IdRegistry registry = new IdRegistry{ salt: salt }();
1616
registry.transferOwnership(initialOwner);
1717
registryAddr = address(registry);
1818
}

src/Bundler.sol

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,14 @@ contract Bundler is Ownable2Step {
8989
* @param recovery Address that is allowed to perform a recovery
9090
* @param storageUnits Number of storage units to rent
9191
*/
92-
function register(address to, address recovery, uint256 storageUnits) external payable {
93-
uint256 fid = idRegistry.register(to, recovery);
92+
function register(
93+
address to,
94+
address recovery,
95+
uint256 deadline,
96+
bytes calldata sig,
97+
uint256 storageUnits
98+
) external payable {
99+
uint256 fid = idRegistry.registerFor(to, recovery, deadline, sig);
94100
uint256 overpayment = storageRent.rent{value: msg.value}(fid, storageUnits);
95101

96102
if (overpayment > 0) {

src/IdRegistry.sol

Lines changed: 109 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// SPDX-License-Identifier: UNLICENSED
22
pragma 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";
48
import {Ownable2Step} from "openzeppelin/contracts/access/Ownable2Step.sol";
5-
import {ERC2771Context} from "openzeppelin-contracts/contracts/metatx/ERC2771Context.sol";
69
import {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
}

test/Bundler/Bundler.gas.t.sol

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,17 @@ import {BundlerTestSuite} from "./BundlerTestSuite.sol";
99
/* solhint-disable state-visibility */
1010

1111
contract BundleRegistryGasUsageTest is BundlerTestSuite {
12-
function testGasRegister() public {
12+
function testGasRegisterWithSig() public {
1313
idRegistry.disableTrustedOnly();
1414

15-
for (uint256 i = 0; i < 10; i++) {
16-
address account = address(uint160(i));
15+
for (uint256 i = 1; i < 10; i++) {
16+
address account = vm.addr(i);
17+
bytes memory sig = _signRegister(i, account, address(0), type(uint40).max);
1718
uint256 price = storageRent.price(1);
1819

1920
vm.deal(account, 10_000 ether);
2021
vm.prank(account);
21-
bundler.register{value: price}(account, address(0), 1);
22+
bundler.register{value: price}(account, address(0), type(uint40).max, sig, 1);
2223
}
2324
}
2425

0 commit comments

Comments
 (0)