├── .solhintignore ├── .lintstagedrc ├── .gitattributes ├── main.rs ├── docs └── audits │ └── 2023-11-05-cyfrin-farcaster-v1.0.pdf ├── .rusty-hook.toml ├── Cargo.toml ├── slither.config.json ├── .solhint.json ├── .gas-snapshot ├── script ├── IdRegistry.s.sol ├── FnameResolver.s.sol ├── KeyRegistry.s.sol ├── DeployL1.s.sol ├── StorageRegistry.s.sol ├── LocalDeploy.s.sol ├── abstract │ └── ImmutableCreate2Deployer.sol └── DeployL2.s.sol ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── src ├── interfaces │ ├── abstract │ │ ├── INonces.sol │ │ ├── ISignatures.sol │ │ ├── IEIP712.sol │ │ ├── IGuardians.sol │ │ └── IMigration.sol │ ├── IMetadataValidator.sol │ ├── IdRegistryLike.sol │ ├── IKeyGateway.sol │ ├── IBundler.sol │ └── IIdGateway.sol ├── abstract │ ├── Nonces.sol │ ├── Signatures.sol │ ├── EIP712.sol │ ├── Guardians.sol │ └── Migration.sol ├── libraries │ ├── TransferHelper.sol │ └── EnumerableKeySet.sol ├── RecoveryProxy.sol ├── Bundler.sol ├── KeyGateway.sol ├── validators │ └── SignedKeyRequestValidator.sol ├── IdGateway.sol └── FnameResolver.sol ├── .vscode └── settings.json ├── test ├── RecoveryProxy │ ├── RecoveryProxyTestSuite.sol │ └── RecoveryProxy.t.sol ├── abstract │ ├── EIP712 │ │ └── EIP712.t.sol │ ├── Guardians │ │ └── Guardians.symbolic.t.sol │ └── Migration │ │ └── Migration.symbolic.t.sol ├── Bundler │ ├── Bundler.gas.t.sol │ └── BundlerTestSuite.sol ├── KeyRegistry │ ├── utils │ │ └── KeyRegistryHarness.sol │ ├── KeyRegistryTestSuite.sol │ ├── KeyRegistryTestHelpers.sol │ └── KeyRegistry.integration.t.sol ├── IdGateway │ ├── IdGateway.gas.t.sol │ ├── IdGatewayTestSuite.sol │ └── IdGateway.owner.t.sol ├── Deploy │ └── DeployL1.t.sol ├── StorageRegistry │ ├── StorageRegistry.gas.t.sol │ └── StorageRegistryTestSuite.sol ├── KeyGateway │ └── KeyGatewayTestSuite.sol ├── validators │ └── SignedKeyRequestValidator │ │ └── SignedKeyRequestValidatorTestSuite.sol ├── IdRegistry │ ├── IdRegistryTestHelpers.sol │ ├── IdRegistryTestSuite.sol │ └── IdRegistry.owner.t.sol ├── FnameResolver │ ├── FnameResolverTestSuite.sol │ └── FnameResolver.t.sol ├── TestSuiteSetup.sol └── Utils.sol ├── .gitignore ├── .gitmodules ├── foundry.toml ├── Dockerfile.foundry ├── .env.prod ├── .env.example ├── .env.upgrade ├── .env.local ├── README.md └── docker-compose.yml /.solhintignore: -------------------------------------------------------------------------------- 1 | # directories 2 | **/lib 3 | **/node_modules 4 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.sol": [ 3 | "forge fmt --check" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | .gas-snapshot linguist-language=Julia 3 | -------------------------------------------------------------------------------- /main.rs: -------------------------------------------------------------------------------- 1 | // an empty cargo target 2 | // we use cargo to install git commit hooks via rusty-hook 3 | fn main() {} 4 | -------------------------------------------------------------------------------- /docs/audits/2023-11-05-cyfrin-farcaster-v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fortunaaaa/contracts/HEAD/docs/audits/2023-11-05-cyfrin-farcaster-v1.0.pdf -------------------------------------------------------------------------------- /.rusty-hook.toml: -------------------------------------------------------------------------------- 1 | [hooks] 2 | pre-commit = "forge fmt --check && forge snapshot --check --match-contract Gas && forge test -vvv" 3 | 4 | [logging] 5 | verbose = true 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "farcasterxyz-contracts" 3 | version = "2.0.0" 4 | edition = "2021" 5 | publish = false 6 | build = "build.rs" 7 | 8 | [[bin]] 9 | name = "main" 10 | path = "main.rs" 11 | 12 | [dependencies] 13 | rusty-hook = "^0.11.2" 14 | -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude_informational": false, 3 | "exclude_low": false, 4 | "exclude_medium": false, 5 | "exclude_high": false, 6 | "disable_color": false, 7 | "filter_paths": "(test/|lib/forge-std/|script/)", 8 | "legacy_ast": false 9 | } 10 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "code-complexity": ["error", 8], 6 | "compiler-version": ["error", ">=0.8.4"], 7 | "func-visibility": ["error", { "ignoreConstructors": true }], 8 | "max-line-length": ["error", 120], 9 | "not-rely-on-time": "off", 10 | "reason-string": ["warn", { "maxLength": 64 }] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gas-snapshot: -------------------------------------------------------------------------------- 1 | BundleRegistryGasUsageTest:testGasRegisterWithSig() (gas: 1118748) 2 | IdGatewayGasUsageTest:testGasRegister() (gas: 1402640) 3 | IdGatewayGasUsageTest:testGasRegisterForAndRecover() (gas: 1973946) 4 | StorageRegistryGasUsageTest:testGasBatchCredit() (gas: 173053) 5 | StorageRegistryGasUsageTest:testGasBatchRent() (gas: 270579) 6 | StorageRegistryGasUsageTest:testGasContinuousCredit() (gas: 166530) 7 | StorageRegistryGasUsageTest:testGasCredit() (gas: 81890) 8 | StorageRegistryGasUsageTest:testGasRent() (gas: 163250) -------------------------------------------------------------------------------- /script/IdRegistry.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {IdRegistry} from "../src/IdRegistry.sol"; 5 | import {ImmutableCreate2Deployer} from "./abstract/ImmutableCreate2Deployer.sol"; 6 | 7 | contract IdRegistryScript is ImmutableCreate2Deployer { 8 | function run() public { 9 | address initialOwner = vm.envAddress("ID_REGISTRY_OWNER_ADDRESS"); 10 | 11 | register("IdRegistry", type(IdRegistry).creationCode, abi.encode(initialOwner)); 12 | deploy(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "bug:" 5 | labels: ["bug"] 6 | assignees: 7 | - varunsrin 8 | --- 9 | 10 | **What is the bug?** 11 | A concise, high level description of the bug and how it affects you 12 | 13 | **How can it be reproduced? (optional)** 14 | Include steps, code samples, replits, screenshots and anything else that would be helpful to reproduce the problem. 15 | 16 | **Additional context (optional)** 17 | Add any other context about the problem here. 18 | -------------------------------------------------------------------------------- /src/interfaces/abstract/INonces.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | interface INonces { 5 | /*////////////////////////////////////////////////////////////// 6 | NONCE MANAGEMENT 7 | //////////////////////////////////////////////////////////////*/ 8 | 9 | /** 10 | * @notice Increase caller's nonce, invalidating previous signatures. 11 | * 12 | * @return uint256 The caller's new nonce. 13 | */ 14 | function useNonce() external returns (uint256); 15 | } 16 | -------------------------------------------------------------------------------- /src/interfaces/abstract/ISignatures.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | interface ISignatures { 5 | /*////////////////////////////////////////////////////////////// 6 | ERRORS 7 | //////////////////////////////////////////////////////////////*/ 8 | 9 | /// @dev Revert when the signature provided is invalid. 10 | error InvalidSignature(); 11 | 12 | /// @dev Revert when the block.timestamp is ahead of the signature deadline. 13 | error SignatureExpired(); 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | ".git": true, 4 | "out": true, 5 | "cache": true, 6 | "lib": true 7 | }, 8 | "editor.formatOnSave": true, 9 | "editor.rulers": [119], 10 | "[json]": { 11 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 12 | }, 13 | "[solidity]": { 14 | "editor.defaultFormatter": "JuanBlanco.solidity" 15 | }, 16 | "solidity.formatter": "forge", 17 | "cSpell.words": ["curr", "Fname", "Seedable", "Pausable", "UUPS"], 18 | "solidity.compileUsingRemoteVersion": "v0.8.19+commit.7dd6d404" 19 | } 20 | -------------------------------------------------------------------------------- /src/interfaces/IMetadataValidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | interface IMetadataValidator { 5 | /** 6 | * @notice Validate metadata associated with a key. 7 | * 8 | * @param userFid The fid associated with the key. 9 | * @param key Bytes of the key. 10 | * @param metadata Metadata about the key. 11 | * 12 | * @return bool Whether the provided key and metadata are valid. 13 | */ 14 | function validate(uint256 userFid, bytes memory key, bytes memory metadata) external returns (bool); 15 | } 16 | -------------------------------------------------------------------------------- /test/RecoveryProxy/RecoveryProxyTestSuite.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {RecoveryProxy} from "../../src/RecoveryProxy.sol"; 5 | import {IdRegistryTestSuite} from "../IdRegistry/IdRegistryTestSuite.sol"; 6 | 7 | /* solhint-disable state-visibility */ 8 | 9 | abstract contract RecoveryProxyTestSuite is IdRegistryTestSuite { 10 | RecoveryProxy recoveryProxy; 11 | 12 | function setUp() public virtual override { 13 | super.setUp(); 14 | 15 | recoveryProxy = new RecoveryProxy(address(idRegistry), owner); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/abstract/Nonces.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | import {Nonces as NoncesBase} from "openzeppelin-latest/contracts/utils/Nonces.sol"; 5 | import {INonces} from "../interfaces/abstract/INonces.sol"; 6 | 7 | abstract contract Nonces is INonces, NoncesBase { 8 | /*////////////////////////////////////////////////////////////// 9 | NONCE MANAGEMENT 10 | //////////////////////////////////////////////////////////////*/ 11 | 12 | /** 13 | * @inheritdoc INonces 14 | */ 15 | function useNonce() external returns (uint256) { 16 | return _useNonce(msg.sender); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/libraries/TransferHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | library TransferHelper { 5 | /// @dev Revert when a native token transfer fails. 6 | error CallFailed(); 7 | 8 | /** 9 | * @dev Native token transfer helper. 10 | */ 11 | function sendNative(address to, uint256 amount) internal { 12 | bool success; 13 | 14 | // solhint-disable-next-line no-inline-assembly 15 | assembly ("memory-safe") { 16 | // Transfer the native token and store if it succeeded or not. 17 | success := call(gas(), to, amount, 0, 0, 0, 0) 18 | } 19 | 20 | if (!success) revert CallFailed(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /script/FnameResolver.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | import {FnameResolver} from "../src/FnameResolver.sol"; 7 | import {ImmutableCreate2Deployer} from "./abstract/ImmutableCreate2Deployer.sol"; 8 | 9 | contract FnameResolverScript is ImmutableCreate2Deployer { 10 | function run() public { 11 | string memory serverURI = vm.envString("FNAME_RESOLVER_SERVER_URL"); 12 | address signer = vm.envAddress("FNAME_RESOLVER_SIGNER_ADDRESS"); 13 | address owner = vm.envAddress("FNAME_RESOLVER_OWNER_ADDRESS"); 14 | 15 | register("FnameResolver", type(FnameResolver).creationCode, abi.encode(serverURI, signer, owner)); 16 | deploy(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .yarn/* 3 | !.yarn/patches 4 | !.yarn/releases 5 | !.yarn/plugins 6 | !.yarn/sdks 7 | !.yarn/versions 8 | **/cache 9 | **/node_modules 10 | **/out 11 | 12 | # files 13 | *.env 14 | *.log 15 | .DS_Store 16 | .pnp.* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | # broadcasts 21 | broadcast/* 22 | 23 | # Rust 24 | # will have compiled files and executables 25 | debug/ 26 | target/ 27 | 28 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 29 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 30 | Cargo.lock 31 | 32 | # These are backup files generated by rustfmt 33 | **/*.rs.bk 34 | 35 | # MSVC Windows builds of rustc generate these, which store debugging information 36 | *.pdb -------------------------------------------------------------------------------- /test/abstract/EIP712/EIP712.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {EIP712} from "../../../src/abstract/EIP712.sol"; 5 | import {TestSuiteSetup} from "../../TestSuiteSetup.sol"; 6 | 7 | /* solhint-disable state-visibility */ 8 | /* solhint-disable no-empty-blocks */ 9 | 10 | contract EIP712Example is EIP712("EIP712 Example", "1") {} 11 | 12 | contract EIP712Test is TestSuiteSetup { 13 | EIP712Example eip712; 14 | 15 | function setUp() public override { 16 | super.setUp(); 17 | 18 | eip712 = new EIP712Example(); 19 | } 20 | 21 | function testExposesDomainSeparator() public { 22 | assertEq(eip712.domainSeparatorV4(), 0x0617e266f62048821cb1d443cca5b7a0e073cb89f23c9f20046cdf79ecb42429); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /script/KeyRegistry.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {KeyRegistry} from "../src/KeyRegistry.sol"; 5 | import {ImmutableCreate2Deployer} from "./abstract/ImmutableCreate2Deployer.sol"; 6 | 7 | contract IdRegistryScript is ImmutableCreate2Deployer { 8 | uint24 internal constant KEY_REGISTRY_MIGRATION_GRACE_PERIOD = 1 days; 9 | 10 | function run() public { 11 | address idRegistry = vm.envAddress("ID_REGISTRY_ADDRESS"); 12 | address initialOwner = vm.envAddress("KEY_REGISTRY_OWNER_ADDRESS"); 13 | 14 | register( 15 | "KeyRegistry", 16 | type(KeyRegistry).creationCode, 17 | abi.encode(idRegistry, KEY_REGISTRY_MIGRATION_GRACE_PERIOD, initialOwner) 18 | ); 19 | 20 | deploy(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | branch = v1 5 | [submodule "lib/solmate"] 6 | path = lib/solmate 7 | url = https://github.com/transmissions11/solmate 8 | branch = v7 9 | [submodule "lib/chainlink-brownie-contracts"] 10 | path = lib/chainlink-brownie-contracts 11 | url = https://github.com/smartcontractkit/chainlink-brownie-contracts 12 | [submodule "lib/openzeppelin-contracts"] 13 | path = lib/openzeppelin-contracts 14 | url = https://github.com/openzeppelin/openzeppelin-contracts 15 | [submodule "lib/openzeppelin-latest"] 16 | path = lib/openzeppelin-latest 17 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 18 | [submodule "lib/halmos-cheatcodes"] 19 | path = lib/halmos-cheatcodes 20 | url = https://github.com/a16z/halmos-cheatcodes 21 | -------------------------------------------------------------------------------- /src/abstract/Signatures.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | import {SignatureChecker} from "openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; 5 | import {ISignatures} from "../interfaces/abstract/ISignatures.sol"; 6 | 7 | abstract contract Signatures is ISignatures { 8 | /*////////////////////////////////////////////////////////////// 9 | SIGNATURE VERIFICATION HELPERS 10 | //////////////////////////////////////////////////////////////*/ 11 | 12 | function _verifySig(bytes32 digest, address signer, uint256 deadline, bytes memory sig) internal view { 13 | if (block.timestamp > deadline) revert SignatureExpired(); 14 | if (!SignatureChecker.isValidSignatureNow(signer, digest, sig)) { 15 | revert InvalidSignature(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/interfaces/abstract/IEIP712.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | interface IEIP712 { 5 | /*////////////////////////////////////////////////////////////// 6 | EIP-712 HELPERS 7 | //////////////////////////////////////////////////////////////*/ 8 | 9 | /** 10 | * @notice Helper view to read EIP-712 domain separator. 11 | * 12 | * @return bytes32 domain separator hash. 13 | */ 14 | function domainSeparatorV4() external view returns (bytes32); 15 | 16 | /** 17 | * @notice Helper view to hash EIP-712 typed data onchain. 18 | * 19 | * @param structHash EIP-712 typed data hash. 20 | * 21 | * @return bytes32 EIP-712 message digest. 22 | */ 23 | function hashTypedDataV4(bytes32 structHash) external view returns (bytes32); 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature 3 | about: "Opening new feature requests " 4 | title: "feat: " 5 | labels: ["feat"] 6 | assignees: 7 | - varunsrin 8 | --- 9 | 10 | **What is the feature you would like to implement?** 11 | A concise, high level summary of the feature 12 | 13 | **Why is this feature important?** 14 | An argument for why this feature should be build and how it should be prioritized 15 | 16 | **Will the protocol spec need to be updated??** 17 | Call out the sections of the [protocol spec](https://github.com/farcasterxyz/protocol) that will need to be updated (e.g. Section 4.3 - Verifications, Section 3.1 - Identity Systems) 18 | 19 | **How should this feature be built? (optional)** 20 | A design for the implementation of the feature 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | solc_version = "0.8.21" 3 | evm_version = "paris" 4 | optimizer_runs = 100_000 5 | fuzz = { runs = 512 } 6 | remappings = [ 7 | "solmate/=lib/solmate/", 8 | "openzeppelin/=lib/openzeppelin-contracts/", 9 | "openzeppelin-latest/=lib/openzeppelin-latest/", 10 | "chainlink/=lib/chainlink-brownie-contracts/contracts/src/" 11 | ] 12 | no_match_path = "test/Deploy/*" 13 | libs = ["node_modules", "lib"] 14 | bytecode_hash = 'none' 15 | 16 | [profile.ci] 17 | verbosity = 3 18 | fuzz = { runs = 2500 } 19 | no_match_path = "" 20 | match_path = "test/*/**" 21 | 22 | [fmt] 23 | line_length = 120 24 | tab_width = 4 25 | quote_style = "double" 26 | bracket_spacing = false 27 | int_types = "long" 28 | multiline_func_header = "params_first" 29 | 30 | [rpc_endpoints] 31 | l2_mainnet = "${L2_MAINNET_RPC_URL}" 32 | l1_mainnet = "${L1_MAINNET_RPC_URL}" 33 | -------------------------------------------------------------------------------- /Dockerfile.foundry: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.4 2 | 3 | # Used for local development so that we have binaries compiled for the local 4 | # architecture, since Foundry only publishes Docker images for AMD, and we don't 5 | # want to have slow emulation on Apple Silicon. 6 | 7 | FROM ubuntu:latest 8 | 9 | RUN apt-get update -y && apt-get install -y bash curl git gzip netcat 10 | 11 | # Foundry only keeps the latest 3 nightly releases available, removing older 12 | # ones. We host the artifacts ourselves to ensure they are always there. 13 | # 14 | # This also makes the build very fast (building from source takes ~10 minutes on an M2 Max) 15 | ARG TARGETARCH COMMIT_HASH=577dae3f632b392856d1d62a5016c765fadd872d 16 | RUN curl --proto '=https' --tlsv1.2 -sSf \ 17 | "https://download.farcaster.xyz/foundry/foundry_${COMMIT_HASH}_linux_${TARGETARCH}.tar.gz" \ 18 | | tar -xzf - -C /usr/local/bin 19 | 20 | ENV RUST_BACKTRACE=full 21 | -------------------------------------------------------------------------------- /src/abstract/EIP712.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | import {EIP712 as EIP712Base} from "openzeppelin/contracts/utils/cryptography/EIP712.sol"; 5 | 6 | import {IEIP712} from "../interfaces/abstract/IEIP712.sol"; 7 | 8 | abstract contract EIP712 is IEIP712, EIP712Base { 9 | constructor(string memory name, string memory version) EIP712Base(name, version) {} 10 | 11 | /*////////////////////////////////////////////////////////////// 12 | EIP-712 HELPERS 13 | //////////////////////////////////////////////////////////////*/ 14 | 15 | /** 16 | * @inheritdoc IEIP712 17 | */ 18 | function domainSeparatorV4() external view returns (bytes32) { 19 | return _domainSeparatorV4(); 20 | } 21 | 22 | /** 23 | * @inheritdoc IEIP712 24 | */ 25 | function hashTypedDataV4(bytes32 structHash) external view returns (bytes32) { 26 | return _hashTypedDataV4(structHash); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Motivation 2 | 3 | Describe why this issue should be fixed and link to any relevant design docs, issues or other relevant items. 4 | 5 | ## Change Summary 6 | 7 | Describe the changes being made in 1-2 concise sentences. 8 | 9 | ## Merge Checklist 10 | 11 | _Choose all relevant options below by adding an `x` now or at any time before submitting for review_ 12 | 13 | - [ ] The PR title adheres to the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) standard 14 | - [ ] The PR has been tagged with change type label(s) (i.e. documentation, feature, bugfix, or chore) 15 | - [ ] The PR's changes adhere to all the requirements in the [contribution guidelines](https://github.com/farcasterxyz/contracts/blob/main/CONTRIBUTING.md#3-proposing-changes) 16 | - [ ] All [commits have been signed](https://github.com/farcasterxyz/contracts/blob/main/CONTRIBUTING.md#22-signing-commits) 17 | 18 | ## Additional Context 19 | 20 | If this is a relatively large or complex change, provide more details here that will help reviewers. 21 | -------------------------------------------------------------------------------- /test/Bundler/Bundler.gas.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import {Bundler, IBundler} from "../../src/Bundler.sol"; 7 | import {BundlerTestSuite} from "./BundlerTestSuite.sol"; 8 | 9 | /* solhint-disable state-visibility */ 10 | 11 | contract BundleRegistryGasUsageTest is BundlerTestSuite { 12 | function setUp() public override { 13 | super.setUp(); 14 | _registerValidator(1, 1); 15 | } 16 | 17 | function testGasRegisterWithSig() public { 18 | for (uint256 i = 1; i < 10; i++) { 19 | address account = vm.addr(i); 20 | bytes memory sig = _signRegister(i, account, address(0), type(uint40).max); 21 | uint256 price = bundler.price(1); 22 | 23 | IBundler.SignerParams[] memory signers = new IBundler.SignerParams[]( 24 | 0 25 | ); 26 | 27 | vm.deal(account, 10_000 ether); 28 | vm.prank(account); 29 | bundler.register{value: price}( 30 | IBundler.RegistrationParams({to: account, recovery: address(0), deadline: type(uint40).max, sig: sig}), 31 | signers, 32 | 1 33 | ); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/interfaces/IdRegistryLike.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | /** 5 | * @dev Minimal interface for IdRegistry, used by the KeyRegistry. 6 | */ 7 | interface IdRegistryLike { 8 | /*////////////////////////////////////////////////////////////// 9 | STORAGE 10 | //////////////////////////////////////////////////////////////*/ 11 | 12 | /** 13 | * @notice Maps each address to an fid, or zero if it does not own an fid. 14 | */ 15 | function idOf(address fidOwner) external view returns (uint256); 16 | 17 | /*////////////////////////////////////////////////////////////// 18 | VIEWS 19 | //////////////////////////////////////////////////////////////*/ 20 | 21 | /** 22 | * @notice Verify that a signature was produced by the custody address that owns an fid. 23 | * 24 | * @param custodyAddress The address to check the signature of. 25 | * @param fid The fid to check the signature of. 26 | * @param digest The digest that was signed. 27 | * @param sig The signature to check. 28 | * 29 | * @return isValid Whether provided signature is valid. 30 | */ 31 | function verifyFidSignature( 32 | address custodyAddress, 33 | uint256 fid, 34 | bytes32 digest, 35 | bytes calldata sig 36 | ) external view returns (bool isValid); 37 | } 38 | -------------------------------------------------------------------------------- /test/KeyRegistry/utils/KeyRegistryHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {KeyRegistry} from "../../../src/KeyRegistry.sol"; 5 | 6 | /** 7 | * @dev Adding enumerable sets tracking keys in the KeyRegistry blew up the 8 | * state space and triggered internal Halmos errors related to symbolic 9 | * storage. This harness class overrides and simplifies internal methods 10 | * that add/remove from KeySet as a workaround, enabling us to test 11 | * the core state transition invariants but ignore KeySet internals. 12 | */ 13 | contract KeyRegistryHarness is KeyRegistry { 14 | constructor( 15 | address idRegistry, 16 | address migrator, 17 | address owner, 18 | uint256 maxKeysPerFid 19 | ) KeyRegistry(idRegistry, migrator, owner, maxKeysPerFid) {} 20 | 21 | mapping(uint256 fid => bytes[] activeKeys) activeKeys; 22 | 23 | function totalKeys(uint256 fid, KeyState) public view override returns (uint256) { 24 | return activeKeys[fid].length; 25 | } 26 | 27 | function _addToKeySet(uint256 fid, bytes calldata key) internal override { 28 | activeKeys[fid].push(key); 29 | } 30 | 31 | function _removeFromKeySet(uint256 fid, bytes calldata) internal override { 32 | activeKeys[fid].pop(); 33 | } 34 | 35 | function _resetFromKeySet(uint256 fid, bytes calldata) internal override { 36 | activeKeys[fid].pop(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/IdGateway/IdGateway.gas.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {IdGatewayTestSuite} from "./IdGatewayTestSuite.sol"; 5 | 6 | /* solhint-disable state-visibility */ 7 | 8 | contract IdGatewayGasUsageTest is IdGatewayTestSuite { 9 | address constant RECOVERY = address(0x6D1217BD164119E2ddE6ce1723879844FD73114e); 10 | 11 | // Perform actions many times to get a good median, since the first run initializes storage 12 | 13 | function testGasRegister() public { 14 | for (uint256 i = 1; i < 15; i++) { 15 | address caller = vm.addr(i); 16 | uint256 fee = idGateway.price(); 17 | vm.deal(caller, fee); 18 | vm.prank(caller); 19 | idGateway.register{value: fee}(RECOVERY); 20 | assertEq(idRegistry.idOf(caller), i); 21 | } 22 | } 23 | 24 | function testGasRegisterForAndRecover() public { 25 | for (uint256 i = 1; i < 15; i++) { 26 | address registrationRecipient = vm.addr(i); 27 | uint40 deadline = type(uint40).max; 28 | 29 | uint256 recoveryRecipientPk = i + 100; 30 | address recoveryRecipient = vm.addr(recoveryRecipientPk); 31 | 32 | uint256 fee = idGateway.price(); 33 | vm.deal(registrationRecipient, fee); 34 | vm.prank(registrationRecipient); 35 | (uint256 fid,) = idGateway.register{value: fee}(RECOVERY); 36 | assertEq(idRegistry.idOf(registrationRecipient), i); 37 | 38 | bytes memory transferSig = _signTransfer(recoveryRecipientPk, fid, recoveryRecipient, deadline); 39 | vm.prank(RECOVERY); 40 | idRegistry.recover(registrationRecipient, recoveryRecipient, deadline, transferSig); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /script/DeployL1.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {FnameResolver} from "../src/FnameResolver.sol"; 5 | import {console, ImmutableCreate2Deployer} from "./abstract/ImmutableCreate2Deployer.sol"; 6 | 7 | contract DeployL1 is ImmutableCreate2Deployer { 8 | bytes32 internal constant FNAME_RESOLVER_CREATE2_SALT = bytes32(0); 9 | 10 | struct DeploymentParams { 11 | string serverURL; 12 | address signer; 13 | address owner; 14 | address deployer; 15 | } 16 | 17 | struct Contracts { 18 | FnameResolver fnameResolver; 19 | } 20 | 21 | function run() public { 22 | runDeploy(loadDeploymentParams()); 23 | } 24 | 25 | function runDeploy(DeploymentParams memory params) public returns (Contracts memory) { 26 | return runDeploy(params, true); 27 | } 28 | 29 | function runDeploy(DeploymentParams memory params, bool broadcast) public returns (Contracts memory) { 30 | address fnameResolver = register( 31 | "FnameResolver", 32 | FNAME_RESOLVER_CREATE2_SALT, 33 | type(FnameResolver).creationCode, 34 | abi.encode(params.serverURL, params.signer, params.owner) 35 | ); 36 | 37 | deploy(broadcast); 38 | 39 | return Contracts({fnameResolver: FnameResolver(fnameResolver)}); 40 | } 41 | 42 | function loadDeploymentParams() internal view returns (DeploymentParams memory) { 43 | return DeploymentParams({ 44 | serverURL: vm.envString("FNAME_RESOLVER_SERVER_URL"), 45 | signer: vm.envAddress("FNAME_RESOLVER_SIGNER_ADDRESS"), 46 | owner: vm.envAddress("FNAME_RESOLVER_OWNER_ADDRESS"), 47 | deployer: vm.envAddress("DEPLOYER") 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /script/StorageRegistry.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {StorageRegistry} from "../src/StorageRegistry.sol"; 5 | import {ImmutableCreate2Deployer} from "./abstract/ImmutableCreate2Deployer.sol"; 6 | 7 | contract StorageRegistryScript is ImmutableCreate2Deployer { 8 | uint256 internal constant INITIAL_RENTAL_PERIOD = 365 days; 9 | uint256 internal constant INITIAL_USD_UNIT_PRICE = 5e8; // $5 USD 10 | uint256 internal constant INITIAL_MAX_UNITS = 2_000_000; 11 | uint256 internal constant INITIAL_PRICE_FEED_CACHE_DURATION = 1 days; 12 | uint256 internal constant INITIAL_UPTIME_FEED_GRACE_PERIOD = 1 hours; 13 | 14 | function run() public { 15 | address priceFeed = vm.envAddress("STORAGE_RENT_PRICE_FEED_ADDRESS"); 16 | address uptimeFeed = vm.envAddress("STORAGE_RENT_UPTIME_FEED_ADDRESS"); 17 | address vault = vm.envAddress("STORAGE_RENT_VAULT_ADDRESS"); 18 | address roleAdmin = vm.envAddress("STORAGE_RENT_ROLE_ADMIN_ADDRESS"); 19 | address admin = vm.envAddress("STORAGE_RENT_ADMIN_ADDRESS"); 20 | address operator = vm.envAddress("STORAGE_RENT_OPERATOR_ADDRESS"); 21 | address treasurer = vm.envAddress("STORAGE_RENT_TREASURER_ADDRESS"); 22 | 23 | register( 24 | "StorageRegistry", 25 | type(StorageRegistry).creationCode, 26 | abi.encode( 27 | priceFeed, 28 | uptimeFeed, 29 | INITIAL_RENTAL_PERIOD, 30 | INITIAL_USD_UNIT_PRICE, 31 | INITIAL_MAX_UNITS, 32 | vault, 33 | roleAdmin, 34 | admin, 35 | operator, 36 | treasurer, 37 | INITIAL_PRICE_FEED_CACHE_DURATION, 38 | INITIAL_UPTIME_FEED_GRACE_PERIOD 39 | ) 40 | ); 41 | 42 | deploy(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/interfaces/abstract/IGuardians.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | interface IGuardians { 5 | /*////////////////////////////////////////////////////////////// 6 | EVENTS 7 | //////////////////////////////////////////////////////////////*/ 8 | 9 | /** 10 | * @dev Emit an event when owner adds a new guardian address. 11 | * 12 | * @param guardian Address of the added guardian. 13 | */ 14 | event Add(address indexed guardian); 15 | 16 | /** 17 | * @dev Emit an event when owner removes a guardian address. 18 | * 19 | * @param guardian Address of the removed guardian. 20 | */ 21 | event Remove(address indexed guardian); 22 | 23 | /*////////////////////////////////////////////////////////////// 24 | ERRORS 25 | //////////////////////////////////////////////////////////////*/ 26 | 27 | /// @dev Revert if an unauthorized caller calls a protected function. 28 | error OnlyGuardian(); 29 | 30 | /*////////////////////////////////////////////////////////////// 31 | PERMISSIONED FUNCTIONS 32 | //////////////////////////////////////////////////////////////*/ 33 | 34 | /** 35 | * @notice Add an address as a guardian. Only callable by owner. 36 | * 37 | * @param guardian Address of the guardian. 38 | */ 39 | function addGuardian(address guardian) external; 40 | 41 | /** 42 | * @notice Remove a guardian. Only callable by owner. 43 | * 44 | * @param guardian Address of the guardian. 45 | */ 46 | function removeGuardian(address guardian) external; 47 | 48 | /** 49 | * @notice Pause the contract. Only callable by owner or a guardian. 50 | */ 51 | function pause() external; 52 | 53 | /** 54 | * @notice Unpause the contract. Only callable by owner. 55 | */ 56 | function unpause() external; 57 | } 58 | -------------------------------------------------------------------------------- /test/KeyRegistry/KeyRegistryTestSuite.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import {IdRegistryTestSuite} from "../IdRegistry/IdRegistryTestSuite.sol"; 7 | import {IMetadataValidator} from "../../src/interfaces/IMetadataValidator.sol"; 8 | import {StubValidator} from "../Utils.sol"; 9 | import {KeyRegistry} from "../../src/KeyRegistry.sol"; 10 | 11 | /* solhint-disable state-visibility */ 12 | 13 | abstract contract KeyRegistryTestSuite is IdRegistryTestSuite { 14 | KeyRegistry internal keyRegistry; 15 | StubValidator internal stubValidator; 16 | 17 | function setUp() public virtual override { 18 | super.setUp(); 19 | 20 | keyRegistry = new KeyRegistry(address(idRegistry), migrator, owner, 10); 21 | stubValidator = new StubValidator(); 22 | 23 | vm.prank(owner); 24 | keyRegistry.unpause(); 25 | 26 | addKnownContract(address(keyRegistry)); 27 | addKnownContract(address(stubValidator)); 28 | } 29 | 30 | function _signRemove( 31 | uint256 pk, 32 | address owner, 33 | bytes memory key, 34 | uint256 deadline 35 | ) internal returns (bytes memory signature) { 36 | bytes32 digest = keyRegistry.hashTypedDataV4( 37 | keccak256( 38 | abi.encode(keyRegistry.REMOVE_TYPEHASH(), owner, keccak256(key), keyRegistry.nonces(owner), deadline) 39 | ) 40 | ); 41 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, digest); 42 | signature = abi.encodePacked(r, s, v); 43 | assertEq(signature.length, 65); 44 | } 45 | 46 | function _registerValidator(uint32 keyType, uint8 typeId) internal { 47 | _registerValidator(keyType, typeId, true); 48 | } 49 | 50 | function _registerValidator(uint32 keyType, uint8 typeId, bool isValid) internal { 51 | vm.prank(owner); 52 | keyRegistry.setValidator(keyType, typeId, IMetadataValidator(address(stubValidator))); 53 | stubValidator.setIsValid(isValid); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/Deploy/DeployL1.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {DeployL1, FnameResolver} from "../../script/DeployL1.s.sol"; 6 | import {IResolverService} from "../../src/FnameResolver.sol"; 7 | import {FnameResolverTestSuite} from "../../test/FnameResolver/FnameResolverTestSuite.sol"; 8 | import "forge-std/console.sol"; 9 | 10 | /* solhint-disable state-visibility */ 11 | 12 | contract DeployL1Test is DeployL1, FnameResolverTestSuite { 13 | address internal deployer = address(this); 14 | address internal alpha = makeAddr("alpha"); 15 | address internal alice = makeAddr("alice"); 16 | 17 | function setUp() public override { 18 | vm.createSelectFork("l1_mainnet"); 19 | 20 | (signer, signerPk) = makeAddrAndKey("signer"); 21 | 22 | DeployL1.DeploymentParams memory params = DeployL1.DeploymentParams({ 23 | serverURL: "https://fnames.farcaster.xyz/ccip/{sender}/{data}.json", 24 | signer: signer, 25 | owner: alpha, 26 | deployer: deployer 27 | }); 28 | 29 | DeployL1.Contracts memory contracts = runDeploy(params, false); 30 | 31 | resolver = contracts.fnameResolver; 32 | } 33 | 34 | function test_deploymentParams() public { 35 | // Check deployment parameters 36 | assertEq(resolver.url(), "https://fnames.farcaster.xyz/ccip/{sender}/{data}.json"); 37 | assertEq(resolver.owner(), alpha); 38 | assertEq(resolver.signers(signer), true); 39 | } 40 | 41 | function test_e2e() public { 42 | uint256 timestamp = block.timestamp - 60; 43 | bytes memory signature = _signProof(signerPk, "alice.fcast.id", timestamp, alice); 44 | bytes memory extraData = abi.encodeCall(IResolverService.resolve, (DNS_ENCODED_NAME, ADDR_QUERY_CALLDATA)); 45 | bytes memory response = 46 | resolver.resolveWithProof(abi.encode("alice.fcast.id", timestamp, alice, signature), extraData); 47 | assertEq(response, abi.encode(alice)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/StorageRegistry/StorageRegistry.gas.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {StorageRegistryTestSuite} from "./StorageRegistryTestSuite.sol"; 5 | 6 | /* solhint-disable state-visibility */ 7 | 8 | contract StorageRegistryGasUsageTest is StorageRegistryTestSuite { 9 | function testGasRent() public { 10 | uint256 units = 1; 11 | uint256 price = storageRegistry.price(units); 12 | 13 | for (uint256 i = 0; i < 10; i++) { 14 | storageRegistry.rent{value: price}(i, units); 15 | } 16 | } 17 | 18 | function testGasBatchRent() public { 19 | uint256[] memory units = new uint256[](5); 20 | units[0] = 1; 21 | units[1] = 1; 22 | units[2] = 1; 23 | units[3] = 1; 24 | units[4] = 1; 25 | 26 | uint256[] memory ids = new uint256[](5); 27 | ids[0] = 1; 28 | ids[1] = 2; 29 | ids[2] = 3; 30 | ids[3] = 4; 31 | ids[4] = 5; 32 | 33 | uint256 totalCost = storageRegistry.price(5); 34 | vm.deal(address(this), totalCost * 10); 35 | 36 | for (uint256 i = 0; i < 10; i++) { 37 | storageRegistry.batchRent{value: totalCost}(ids, units); 38 | } 39 | } 40 | 41 | function testGasCredit() public { 42 | uint256 units = 1; 43 | 44 | for (uint256 i = 0; i < 10; i++) { 45 | vm.prank(operator); 46 | storageRegistry.credit(1, units); 47 | } 48 | } 49 | 50 | function testGasBatchCredit() public { 51 | uint256[] memory ids = new uint256[](5); 52 | ids[0] = 1; 53 | ids[1] = 2; 54 | ids[2] = 3; 55 | ids[3] = 4; 56 | ids[4] = 5; 57 | 58 | for (uint256 i = 0; i < 10; i++) { 59 | vm.prank(operator); 60 | storageRegistry.batchCredit(ids, 1); 61 | } 62 | } 63 | 64 | function testGasContinuousCredit() public { 65 | for (uint256 i = 0; i < 10; i++) { 66 | vm.prank(operator); 67 | storageRegistry.continuousCredit(1, 5, 1); 68 | } 69 | } 70 | 71 | // solhint-disable-next-line no-empty-blocks 72 | receive() external payable {} 73 | } 74 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | # Price feed addresses 2 | STORAGE_RENT_PRICE_FEED_ADDRESS=0x13e3ee699d1909e989722e753853ae30b17e08c5 3 | STORAGE_RENT_UPTIME_FEED_ADDRESS=0x371EAD81c9102C9BF4874A9075FFFf170F2Ee389 4 | 5 | # Storage rent params 6 | STORAGE_RENT_ROLE_ADMIN_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 7 | STORAGE_RENT_VAULT_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 8 | STORAGE_RENT_ADMIN_ADDRESS=0xD84E32224A249A575A09672Da9cb58C381C4837a 9 | STORAGE_RENT_OPERATOR_ADDRESS=0x0000000000000000000000000000000000000000 10 | STORAGE_RENT_TREASURER_ADDRESS=0x0000000000000000000000000000000000000000 11 | 12 | # ID registry params 13 | ID_REGISTRY_OWNER_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 14 | 15 | # Key registry params 16 | KEY_REGISTRY_OWNER_ADDRESS=0x2D93c2F74b2C4697f9ea85D0450148AA45D4D5a2 17 | 18 | # Metadata validator params 19 | METADATA_VALIDATOR_OWNER_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 20 | 21 | # Bundler params 22 | BUNDLER_TRUSTED_CALLER_ADDRESS=0x2D93c2F74b2C4697f9ea85D0450148AA45D4D5a2 23 | BUNDLER_OWNER_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 24 | 25 | # Recovery proxy params 26 | RECOVERY_PROXY_OWNER_ADDRESS=0xFFE52568Fb0E7038Ef289677288BB704E5c9E82e 27 | 28 | # Fname resolver params. 29 | FNAME_RESOLVER_SERVER_URL=https://fnames.farcaster.xyz/ccip/{sender}/{data}.json 30 | FNAME_RESOLVER_SIGNER_ADDRESS=0xBc5274eFc266311015793d89E9B591fa46294741 31 | FNAME_RESOLVER_OWNER_ADDRESS=0x138356f24c7A16BE48978dE277a468F6C16A19a5 32 | 33 | # RPC endpoints for OP testnet and mainnet. 34 | L1_MAINNET_RPC_URL= 35 | L2_MAINNET_RPC_URL= 36 | 37 | # Salts 38 | STORAGE_RENT_CREATE2_SALT=0x6d2b70e39c6bc63763098e336323591eb77cd0c65360d99ba6ea4e0161f2b96c 39 | ID_REGISTRY_CREATE2_SALT=0x6d2b70e39c6bc63763098e336323591eb77cd0c63e0688f6d95afa008febf4d7 40 | KEY_REGISTRY_CREATE2_SALT=0x6d2b70e39c6bc63763098e336323591eb77cd0c62af4de6e1f0355029f357f47 41 | SIGNED_KEY_REQUEST_VALIDATOR_CREATE2_SALT=0x6d2b70e39c6bc63763098e336323591eb77cd0c6610c0841333604016684800c 42 | BUNDLER_CREATE2_SALT=0x6d2b70e39c6bc63763098e336323591eb77cd0c6e451fc0a34ec4c008c9a31fa 43 | RECOVERY_PROXY_CREATE2_SALT=0x6d2b70e39c6bc63763098e336323591eb77cd0c6110eaaca06f77900dac1cad3 44 | 45 | # Deployer address. 46 | DEPLOYER=0x6D2b70e39C6bc63763098e336323591eb77Cd0C6 47 | 48 | # Migrator address. 49 | MIGRATOR_ADDRESS=0x2D93c2F74b2C4697f9ea85D0450148AA45D4D5a2 50 | -------------------------------------------------------------------------------- /test/KeyGateway/KeyGatewayTestSuite.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import {KeyRegistryTestSuite} from "../KeyRegistry/KeyRegistryTestSuite.sol"; 7 | import {StorageRegistryTestSuite} from "../StorageRegistry/StorageRegistryTestSuite.sol"; 8 | import {KeyGateway} from "../../src/KeyGateway.sol"; 9 | 10 | /* solhint-disable state-visibility */ 11 | 12 | abstract contract KeyGatewayTestSuite is KeyRegistryTestSuite, StorageRegistryTestSuite { 13 | KeyGateway internal keyGateway; 14 | 15 | function setUp() public virtual override(KeyRegistryTestSuite, StorageRegistryTestSuite) { 16 | super.setUp(); 17 | 18 | keyGateway = new KeyGateway(address(keyRegistry), owner); 19 | 20 | vm.startPrank(owner); 21 | keyRegistry.setKeyGateway(address(keyGateway)); 22 | vm.stopPrank(); 23 | 24 | addKnownContract(address(keyGateway)); 25 | } 26 | 27 | function _signAdd( 28 | uint256 pk, 29 | address owner, 30 | uint32 keyType, 31 | bytes memory key, 32 | uint8 metadataType, 33 | bytes memory metadata, 34 | uint256 deadline 35 | ) internal returns (bytes memory signature) { 36 | return _signAdd(pk, owner, keyType, key, metadataType, metadata, keyGateway.nonces(owner), deadline); 37 | } 38 | 39 | function _signAdd( 40 | uint256 pk, 41 | address owner, 42 | uint32 keyType, 43 | bytes memory key, 44 | uint8 metadataType, 45 | bytes memory metadata, 46 | uint256 nonce, 47 | uint256 deadline 48 | ) internal returns (bytes memory signature) { 49 | bytes32 digest = keyGateway.hashTypedDataV4( 50 | keccak256( 51 | abi.encode( 52 | keyGateway.ADD_TYPEHASH(), 53 | owner, 54 | keyType, 55 | keccak256(key), 56 | metadataType, 57 | keccak256(metadata), 58 | nonce, 59 | deadline 60 | ) 61 | ) 62 | ); 63 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, digest); 64 | signature = abi.encodePacked(r, s, v); 65 | assertEq(signature.length, 65); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Price feed addresses 2 | STORAGE_RENT_PRICE_FEED_ADDRESS=0x13e3ee699d1909e989722e753853ae30b17e08c5 3 | STORAGE_RENT_UPTIME_FEED_ADDRESS=0x371EAD81c9102C9BF4874A9075FFFf170F2Ee389 4 | 5 | # Storage rent params 6 | STORAGE_RENT_ROLE_ADMIN_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 7 | STORAGE_RENT_VAULT_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 8 | STORAGE_RENT_ADMIN_ADDRESS=0xD84E32224A249A575A09672Da9cb58C381C4837a 9 | STORAGE_RENT_OPERATOR_ADDRESS=0x0000000000000000000000000000000000000000 10 | STORAGE_RENT_TREASURER_ADDRESS=0x0000000000000000000000000000000000000000 11 | 12 | # ID registry params 13 | ID_REGISTRY_OWNER_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 14 | 15 | # Key registry params 16 | KEY_REGISTRY_OWNER_ADDRESS=0x2D93c2F74b2C4697f9ea85D0450148AA45D4D5a2 17 | 18 | # Metadata validator params 19 | METADATA_VALIDATOR_OWNER_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 20 | 21 | # Bundler params 22 | BUNDLER_TRUSTED_CALLER_ADDRESS=0x2D93c2F74b2C4697f9ea85D0450148AA45D4D5a2 23 | BUNDLER_OWNER_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 24 | 25 | # Recovery proxy params 26 | RECOVERY_PROXY_OWNER_ADDRESS=0xFFE52568Fb0E7038Ef289677288BB704E5c9E82e 27 | 28 | # Fname resolver params. 29 | FNAME_RESOLVER_SERVER_URL=https://fnames.farcaster.xyz/ccip/{sender}/{data}.json 30 | FNAME_RESOLVER_SIGNER_ADDRESS=0xBc5274eFc266311015793d89E9B591fa46294741 31 | FNAME_RESOLVER_OWNER_ADDRESS=0x138356f24c7A16BE48978dE277a468F6C16A19a5 32 | 33 | # RPC endpoints for OP testnet and mainnet. 34 | L1_MAINNET_RPC_URL= 35 | L2_MAINNET_RPC_URL= 36 | 37 | # Salts 38 | STORAGE_RENT_CREATE2_SALT=0x6d2b70e39c6bc63763098e336323591eb77cd0c65360d99ba6ea4e0161f2b96c 39 | ID_REGISTRY_CREATE2_SALT=0x6d2b70e39c6bc63763098e336323591eb77cd0c63e0688f6d95afa008febf4d7 40 | KEY_REGISTRY_CREATE2_SALT=0x6d2b70e39c6bc63763098e336323591eb77cd0c62af4de6e1f0355029f357f47 41 | SIGNED_KEY_REQUEST_VALIDATOR_CREATE2_SALT=0x6d2b70e39c6bc63763098e336323591eb77cd0c6610c0841333604016684800c 42 | BUNDLER_CREATE2_SALT=0x6d2b70e39c6bc63763098e336323591eb77cd0c6e451fc0a34ec4c008c9a31fa 43 | RECOVERY_PROXY_CREATE2_SALT=0x6d2b70e39c6bc63763098e336323591eb77cd0c6110eaaca06f77900dac1cad3 44 | 45 | # Deployer address. 46 | DEPLOYER=0x6D2b70e39C6bc63763098e336323591eb77Cd0C6 47 | 48 | # Default migrator address. 49 | MIGRATOR_ADDRESS=0x2D93c2F74b2C4697f9ea85D0450148AA45D4D5a2 50 | -------------------------------------------------------------------------------- /test/validators/SignedKeyRequestValidator/SignedKeyRequestValidatorTestSuite.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {IdRegistryTestSuite} from "../../IdRegistry/IdRegistryTestSuite.sol"; 5 | 6 | import {SignedKeyRequestValidator} from "../../../src/validators/SignedKeyRequestValidator.sol"; 7 | 8 | /* solhint-disable state-visibility */ 9 | 10 | abstract contract SignedKeyRequestValidatorTestSuite is IdRegistryTestSuite { 11 | SignedKeyRequestValidator internal validator; 12 | 13 | function setUp() public virtual override { 14 | super.setUp(); 15 | 16 | validator = new SignedKeyRequestValidator(address(idRegistry), owner); 17 | } 18 | 19 | function _validKey(bytes memory keyBytes) internal pure returns (bytes memory) { 20 | if (keyBytes.length < 32) { 21 | // pad with zero bytes 22 | bytes memory padding = new bytes(32 - keyBytes.length); 23 | return bytes.concat(keyBytes, padding); 24 | } else if (keyBytes.length > 32) { 25 | // truncate length 26 | assembly { 27 | mstore(keyBytes, 32) 28 | } 29 | return keyBytes; 30 | } else { 31 | return keyBytes; 32 | } 33 | } 34 | 35 | function _shortKey(bytes memory keyBytes, uint8 _amount) internal view returns (bytes memory) { 36 | uint256 amount = bound(_amount, 0, 31); 37 | assembly { 38 | mstore(keyBytes, amount) 39 | } 40 | return keyBytes; 41 | } 42 | 43 | function _longKey(bytes memory keyBytes, uint8 _amount) internal view returns (bytes memory) { 44 | uint256 amount = bound(_amount, 1, type(uint8).max); 45 | bytes memory padding = new bytes(amount); 46 | return bytes.concat(_validKey(keyBytes), padding); 47 | } 48 | 49 | function _signMetadata( 50 | uint256 pk, 51 | uint256 requestingFid, 52 | bytes memory signerPubKey, 53 | uint256 deadline 54 | ) internal returns (bytes memory signature) { 55 | bytes32 digest = validator.hashTypedDataV4( 56 | keccak256(abi.encode(validator.METADATA_TYPEHASH(), requestingFid, keccak256(signerPubKey), deadline)) 57 | ); 58 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, digest); 59 | signature = abi.encodePacked(r, s, v); 60 | assertEq(signature.length, 65); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/abstract/Guardians.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | import {Ownable2Step} from "openzeppelin/contracts/access/Ownable2Step.sol"; 5 | import {Pausable} from "openzeppelin/contracts/security/Pausable.sol"; 6 | 7 | import {IGuardians} from "../interfaces/abstract/IGuardians.sol"; 8 | 9 | abstract contract Guardians is IGuardians, Ownable2Step, Pausable { 10 | /** 11 | * @notice Mapping of addresses to guardian status. 12 | */ 13 | mapping(address guardian => bool isGuardian) public guardians; 14 | 15 | /*////////////////////////////////////////////////////////////// 16 | MODIFIERS 17 | //////////////////////////////////////////////////////////////*/ 18 | 19 | /** 20 | * @notice Allow only the owner or a guardian to call the 21 | * protected function. 22 | */ 23 | modifier onlyGuardian() { 24 | if (msg.sender != owner() && !guardians[msg.sender]) { 25 | revert OnlyGuardian(); 26 | } 27 | _; 28 | } 29 | 30 | /*////////////////////////////////////////////////////////////// 31 | CONSTRUCTOR 32 | //////////////////////////////////////////////////////////////*/ 33 | 34 | /** 35 | * @notice Set the initial owner address. 36 | * 37 | * @param _initialOwner Address of the contract owner. 38 | */ 39 | constructor(address _initialOwner) { 40 | _transferOwnership(_initialOwner); 41 | } 42 | 43 | /*////////////////////////////////////////////////////////////// 44 | PERMISSIONED FUNCTIONS 45 | //////////////////////////////////////////////////////////////*/ 46 | 47 | /** 48 | * @inheritdoc IGuardians 49 | */ 50 | function addGuardian(address guardian) external onlyOwner { 51 | guardians[guardian] = true; 52 | emit Add(guardian); 53 | } 54 | 55 | /** 56 | * @inheritdoc IGuardians 57 | */ 58 | function removeGuardian(address guardian) external onlyOwner { 59 | guardians[guardian] = false; 60 | emit Remove(guardian); 61 | } 62 | 63 | /** 64 | * @inheritdoc IGuardians 65 | */ 66 | function pause() external onlyGuardian { 67 | _pause(); 68 | } 69 | 70 | /** 71 | * @inheritdoc IGuardians 72 | */ 73 | function unpause() external onlyOwner { 74 | _unpause(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/IdRegistry/IdRegistryTestHelpers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {IIdRegistry} from "../../src/interfaces/IIdRegistry.sol"; 5 | 6 | library BulkRegisterDataBuilder { 7 | function empty() internal pure returns (IIdRegistry.BulkRegisterData[] memory) { 8 | return new IIdRegistry.BulkRegisterData[](0); 9 | } 10 | 11 | function addFid( 12 | IIdRegistry.BulkRegisterData[] memory addData, 13 | uint24 fid 14 | ) internal pure returns (IIdRegistry.BulkRegisterData[] memory) { 15 | IIdRegistry.BulkRegisterData[] memory newData = new IIdRegistry.BulkRegisterData[](addData.length + 1); 16 | for (uint256 i; i < addData.length; i++) { 17 | newData[i] = addData[i]; 18 | } 19 | newData[addData.length].fid = fid; 20 | newData[addData.length].custody = address(uint160(uint256(keccak256(abi.encodePacked(fid))))); 21 | newData[addData.length].recovery = 22 | address(uint160(uint256(keccak256(abi.encodePacked(keccak256(abi.encodePacked(fid))))))); 23 | return newData; 24 | } 25 | 26 | function custodyOf(uint24 fid) internal pure returns (address) { 27 | return address(uint160(uint256(keccak256(abi.encodePacked(fid))))); 28 | } 29 | 30 | function recoveryOf(uint24 fid) internal pure returns (address) { 31 | return address(uint160(uint256(keccak256(abi.encodePacked(keccak256(abi.encodePacked(fid))))))); 32 | } 33 | } 34 | 35 | library BulkRegisterDefaultRecoveryDataBuilder { 36 | function empty() internal pure returns (IIdRegistry.BulkRegisterDefaultRecoveryData[] memory) { 37 | return new IIdRegistry.BulkRegisterDefaultRecoveryData[](0); 38 | } 39 | 40 | function addFid( 41 | IIdRegistry.BulkRegisterDefaultRecoveryData[] memory addData, 42 | uint24 fid 43 | ) internal pure returns (IIdRegistry.BulkRegisterDefaultRecoveryData[] memory) { 44 | IIdRegistry.BulkRegisterDefaultRecoveryData[] memory newData = 45 | new IIdRegistry.BulkRegisterDefaultRecoveryData[](addData.length + 1); 46 | for (uint256 i; i < addData.length; i++) { 47 | newData[i] = addData[i]; 48 | } 49 | newData[addData.length].fid = fid; 50 | newData[addData.length].custody = address(uint160(uint256(keccak256(abi.encodePacked(fid))))); 51 | return newData; 52 | } 53 | 54 | function custodyOf(uint24 fid) internal pure returns (address) { 55 | return address(uint160(uint256(keccak256(abi.encodePacked(fid))))); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.env.upgrade: -------------------------------------------------------------------------------- 1 | # Price feed addresses 2 | STORAGE_RENT_PRICE_FEED_ADDRESS=0x13e3ee699d1909e989722e753853ae30b17e08c5 3 | STORAGE_RENT_UPTIME_FEED_ADDRESS=0x371EAD81c9102C9BF4874A9075FFFf170F2Ee389 4 | 5 | # Storage rent params 6 | STORAGE_RENT_ROLE_ADMIN_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 7 | STORAGE_RENT_VAULT_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 8 | STORAGE_RENT_ADMIN_ADDRESS=0xD84E32224A249A575A09672Da9cb58C381C4837a 9 | STORAGE_RENT_OPERATOR_ADDRESS=0x0000000000000000000000000000000000000000 10 | STORAGE_RENT_TREASURER_ADDRESS=0x0000000000000000000000000000000000000000 11 | 12 | # ID registry params 13 | ID_REGISTRY_OWNER_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 14 | 15 | # Key registry params 16 | KEY_REGISTRY_OWNER_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 17 | 18 | # Metadata validator params 19 | METADATA_VALIDATOR_OWNER_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692 20 | 21 | # Bundler params 22 | BUNDLER_TRUSTED_CALLER_ADDRESS= 23 | BUNDLER_OWNER_ADDRESS= 24 | 25 | # Recovery proxy params 26 | RECOVERY_PROXY_OWNER_ADDRESS=0xFFE52568Fb0E7038Ef289677288BB704E5c9E82e 27 | 28 | # Fname resolver params. 29 | FNAME_RESOLVER_SERVER_URL=https://fnames.farcaster.xyz/ccip/{sender}/{data}.json 30 | FNAME_RESOLVER_SIGNER_ADDRESS=0xBc5274eFc266311015793d89E9B591fa46294741 31 | FNAME_RESOLVER_OWNER_ADDRESS=0x138356f24c7A16BE48978dE277a468F6C16A19a5 32 | 33 | # RPC endpoints for OP testnet and mainnet. 34 | L1_MAINNET_RPC_URL= 35 | L2_MAINNET_RPC_URL= 36 | 37 | # Salts 38 | STORAGE_RENT_CREATE2_SALT=0x0000000000000000000000000000000000000000000000000000000000000000 39 | ID_REGISTRY_CREATE2_SALT=0x299707e127cc77de01b9fd968bc0ff475f3c6342d7872c397cd084029fbf64dc 40 | ID_GATEWAY_CREATE2_SALT=0x299707e127cc77de01b9fd968bc0ff475f3c6342a1b2a1fd9db0df01f0373563 41 | KEY_REGISTRY_CREATE2_SALT=0x299707e127cc77de01b9fd968bc0ff475f3c6342aee9be2412b02b01eb294554 42 | KEY_GATEWAY_CREATE2_SALT=0x299707e127cc77de01b9fd968bc0ff475f3c6342229ded5ec3c3bd02e574f7be 43 | SIGNED_KEY_REQUEST_VALIDATOR_CREATE2_SALT=0x0000000000000000000000000000000000000000000000000000000000000000 44 | BUNDLER_CREATE2_SALT=0x299707e127cc77de01b9fd968bc0ff475f3c6342e9da01d98917640342e02a5c 45 | RECOVERY_PROXY_CREATE2_SALT=0x299707e127cc77de01b9fd968bc0ff475f3c63421958ea987b94fd038a490454 46 | 47 | # Deployed contracts 48 | STORAGE_RENT_ADDRESS=0x00000000fcCe7f938e7aE6D3c335bD6a1a7c593D 49 | SIGNED_KEY_REQUEST_VALIDATOR_ADDRESS=0x00000000FC700472606ED4fA22623Acf62c60553 50 | 51 | # Deployer address. 52 | DEPLOYER=0x299707E127CC77DE01b9Fd968Bc0ff475f3C6342 53 | 54 | # Migrator address. 55 | MIGRATOR_ADDRESS=0x2D93c2F74b2C4697f9ea85D0450148AA45D4D5a2 56 | -------------------------------------------------------------------------------- /src/interfaces/IKeyGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import {IKeyRegistry} from "./IKeyRegistry.sol"; 5 | 6 | interface IKeyGateway { 7 | /*////////////////////////////////////////////////////////////// 8 | CONSTANTS 9 | //////////////////////////////////////////////////////////////*/ 10 | 11 | /** 12 | * @notice Contract version specified in the Farcaster protocol version scheme. 13 | */ 14 | function VERSION() external view returns (string memory); 15 | 16 | /** 17 | * @notice EIP-712 typehash for Add signatures. 18 | */ 19 | function ADD_TYPEHASH() external view returns (bytes32); 20 | 21 | /*////////////////////////////////////////////////////////////// 22 | STORAGE 23 | //////////////////////////////////////////////////////////////*/ 24 | 25 | /** 26 | * @notice The KeyRegistry contract. 27 | */ 28 | function keyRegistry() external view returns (IKeyRegistry); 29 | 30 | /*////////////////////////////////////////////////////////////// 31 | REGISTRATION 32 | //////////////////////////////////////////////////////////////*/ 33 | 34 | /** 35 | * @notice Add a key associated with the caller's fid, setting the key state to ADDED. 36 | * 37 | * @param keyType The key's numeric keyType. 38 | * @param key Bytes of the key to add. 39 | * @param metadataType Metadata type ID. 40 | * @param metadata Metadata about the key, which is not stored and only emitted in an event. 41 | */ 42 | function add(uint32 keyType, bytes calldata key, uint8 metadataType, bytes calldata metadata) external; 43 | 44 | /** 45 | * @notice Add a key on behalf of another fid owner, setting the key state to ADDED. 46 | * caller must supply a valid EIP-712 Add signature from the fid owner. 47 | * 48 | * @param fidOwner The fid owner address. 49 | * @param keyType The key's numeric keyType. 50 | * @param key Bytes of the key to add. 51 | * @param metadataType Metadata type ID. 52 | * @param metadata Metadata about the key, which is not stored and only emitted in an event. 53 | * @param deadline Deadline after which the signature expires. 54 | * @param sig EIP-712 Add signature generated by fid owner. 55 | */ 56 | function addFor( 57 | address fidOwner, 58 | uint32 keyType, 59 | bytes calldata key, 60 | uint8 metadataType, 61 | bytes calldata metadata, 62 | uint256 deadline, 63 | bytes calldata sig 64 | ) external; 65 | } 66 | -------------------------------------------------------------------------------- /test/IdGateway/IdGatewayTestSuite.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {TestSuiteSetup} from "../TestSuiteSetup.sol"; 5 | import {StorageRegistryTestSuite} from "../StorageRegistry/StorageRegistryTestSuite.sol"; 6 | import {IdRegistryTestSuite} from "../IdRegistry/IdRegistryTestSuite.sol"; 7 | import {KeyRegistryTestSuite} from "../KeyRegistry/KeyRegistryTestSuite.sol"; 8 | 9 | import {IdGateway} from "../../src/IdGateway.sol"; 10 | 11 | /* solhint-disable state-visibility */ 12 | 13 | abstract contract IdGatewayTestSuite is StorageRegistryTestSuite, KeyRegistryTestSuite { 14 | IdGateway idGateway; 15 | 16 | function setUp() public virtual override(StorageRegistryTestSuite, KeyRegistryTestSuite) { 17 | super.setUp(); 18 | 19 | idGateway = new IdGateway( 20 | address(idRegistry), 21 | address(storageRegistry), 22 | owner 23 | ); 24 | 25 | vm.startPrank(owner); 26 | idRegistry.setIdGateway(address(idGateway)); 27 | vm.stopPrank(); 28 | 29 | addKnownContract(address(idGateway)); 30 | } 31 | 32 | function _registerTo(address caller) internal returns (uint256 fid) { 33 | fid = _registerWithRecovery(caller, address(0)); 34 | } 35 | 36 | function _registerToWithRecovery(address caller, address recovery) internal returns (uint256 fid) { 37 | vm.prank(caller); 38 | (fid,) = idGateway.register(recovery); 39 | } 40 | 41 | function _registerFor(uint256 callerPk, uint40 _deadline) internal { 42 | _registerForWithRecovery(callerPk, address(0), _deadline); 43 | } 44 | 45 | function _registerForWithRecovery(uint256 callerPk, address recovery, uint40 _deadline) internal { 46 | uint256 deadline = _boundDeadline(_deadline); 47 | callerPk = _boundPk(callerPk); 48 | 49 | address caller = vm.addr(callerPk); 50 | bytes memory sig = _signRegister(callerPk, caller, recovery, deadline); 51 | 52 | vm.prank(caller); 53 | idGateway.registerFor(caller, recovery, deadline, sig); 54 | } 55 | 56 | function _signRegister( 57 | uint256 pk, 58 | address to, 59 | address recovery, 60 | uint256 deadline 61 | ) internal returns (bytes memory signature) { 62 | address signer = vm.addr(pk); 63 | bytes32 digest = idGateway.hashTypedDataV4( 64 | keccak256(abi.encode(idGateway.REGISTER_TYPEHASH(), to, recovery, idGateway.nonces(signer), deadline)) 65 | ); 66 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, digest); 67 | signature = abi.encodePacked(r, s, v); 68 | assertEq(signature.length, 65); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | # Price feed addresses 2 | # These defaults are the Optimism mainnet addresses. 3 | STORAGE_RENT_PRICE_FEED_ADDRESS=0x13e3ee699d1909e989722e753853ae30b17e08c5 4 | STORAGE_RENT_UPTIME_FEED_ADDRESS=0x371EAD81c9102C9BF4874A9075FFFf170F2Ee389 5 | 6 | # Storage rent params 7 | # Default address is Anvil test account 1 8 | STORAGE_RENT_ROLE_ADMIN_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 9 | STORAGE_RENT_VAULT_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 10 | STORAGE_RENT_ADMIN_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 11 | STORAGE_RENT_OPERATOR_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 12 | STORAGE_RENT_TREASURER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 13 | 14 | # ID registry params 15 | # Default address is Anvil test account 1 16 | ID_REGISTRY_OWNER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 17 | 18 | # Key registry params 19 | # Default address is Anvil test account 1 20 | KEY_REGISTRY_OWNER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 21 | 22 | # Metadata validator params 23 | # Default address is Anvil test account 1 24 | METADATA_VALIDATOR_OWNER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 25 | 26 | # Bundler params 27 | # Default addresses are Anvil test account 1 28 | BUNDLER_TRUSTED_CALLER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 29 | BUNDLER_OWNER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 30 | 31 | # RecoveryProxy params 32 | # Default addresses are Anvil test account 1 33 | RECOVERY_PROXY_OWNER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 34 | 35 | # Fname resolver params. 36 | # Default owner is Anvil test account 1 37 | FNAME_RESOLVER_SERVER_URL=https://fnames.farcaster.xyz/ccip/{sender}/{data}.json 38 | FNAME_RESOLVER_SIGNER_ADDRESS=0xBc5274eFc266311015793d89E9B591fa46294741 39 | FNAME_RESOLVER_OWNER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 40 | 41 | # RPC endpoints for OP testnet and mainnet. 42 | L2_MAINNET_RPC_URL= 43 | L1_MAINNET_RPC_URL= 44 | 45 | STORAGE_RENT_CREATE2_SALT=0x0000000000000000000000000000000000000000000000000000000000000000 46 | ID_REGISTRY_CREATE2_SALT=0x0000000000000000000000000000000000000000000000000000000000000000 47 | KEY_REGISTRY_CREATE2_SALT=0x0000000000000000000000000000000000000000000000000000000000000000 48 | SIGNED_KEY_REQUEST_VALIDATOR_CREATE2_SALT=0x0000000000000000000000000000000000000000000000000000000000000000 49 | BUNDLER_CREATE2_SALT=0x0000000000000000000000000000000000000000000000000000000000000000 50 | RECOVERY_PROXY_CREATE2_SALT=0x0000000000000000000000000000000000000000000000000000000000000000 51 | 52 | # Deployer address. 53 | # Default address is Anvil test account 1 54 | DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 55 | 56 | # Migrator address. 57 | # Default address is Anvil test account 1 58 | MIGRATOR_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 59 | -------------------------------------------------------------------------------- /test/FnameResolver/FnameResolverTestSuite.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {EIP712} from "openzeppelin/contracts/utils/cryptography/EIP712.sol"; 5 | 6 | import {TestSuiteSetup} from "../TestSuiteSetup.sol"; 7 | import {FnameResolver} from "../../src/FnameResolver.sol"; 8 | 9 | /* solhint-disable state-visibility */ 10 | 11 | abstract contract FnameResolverTestSuite is TestSuiteSetup { 12 | FnameResolver internal resolver; 13 | 14 | string internal constant FNAME_SERVER_URL = "https://fnames.fcast.id/ccip/{sender}/{data}.json"; 15 | 16 | /** 17 | * @dev DNS-encoding of "alice.fcast.id". The DNS-encoded name consists of: 18 | * - 1 byte for the length of the first label (5) 19 | * - 5 bytes for the label ("alice") 20 | * - 1 byte for the length of the second label (5) 21 | * - 5 bytes for the label ("fcast") 22 | * - 1 byte for the length of the third label (2) 23 | * - 2 bytes for the label ("id") 24 | * - A null byte terminating the encoded name. 25 | */ 26 | bytes internal constant DNS_ENCODED_NAME = 27 | (hex"05" hex"616c696365" hex"05" hex"6663617374" hex"02" hex"6964" hex"00"); 28 | 29 | /** 30 | * @dev Encoded calldata for a call to addr(bytes32 node), where node is the ENS 31 | * nameHash encoded value of "alice.fcast.id" 32 | */ 33 | bytes internal constant ADDR_QUERY_CALLDATA = hex"c30dc5a16498c5b6d46f97ca0c74d092ebbee1290b1c88f6e435dd4fb306ca36"; 34 | 35 | address internal signer; 36 | uint256 internal signerPk; 37 | 38 | address internal mallory; 39 | uint256 internal malloryPk; 40 | 41 | function setUp() public virtual override { 42 | (signer, signerPk) = makeAddrAndKey("signer"); 43 | (mallory, malloryPk) = makeAddrAndKey("mallory"); 44 | resolver = new FnameResolver(FNAME_SERVER_URL, signer, owner); 45 | } 46 | 47 | /*////////////////////////////////////////////////////////////// 48 | HELPERS 49 | //////////////////////////////////////////////////////////////*/ 50 | 51 | function _signProof( 52 | string memory name, 53 | uint256 timestamp, 54 | address owner 55 | ) internal returns (bytes memory signature) { 56 | return _signProof(signerPk, name, timestamp, owner); 57 | } 58 | 59 | function _signProof( 60 | uint256 pk, 61 | string memory name, 62 | uint256 timestamp, 63 | address owner 64 | ) internal returns (bytes memory signature) { 65 | bytes32 eip712hash = resolver.hashTypedDataV4( 66 | keccak256(abi.encode(resolver.USERNAME_PROOF_TYPEHASH(), keccak256(bytes(name)), timestamp, owner)) 67 | ); 68 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, eip712hash); 69 | signature = abi.encodePacked(r, s, v); 70 | assertEq(signature.length, 65); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/Bundler/BundlerTestSuite.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {TestSuiteSetup} from "../TestSuiteSetup.sol"; 5 | import {IdGatewayTestSuite} from "../IdGateway/IdGatewayTestSuite.sol"; 6 | 7 | import {KeyGateway} from "../../src/KeyGateway.sol"; 8 | import {Bundler} from "../../src/Bundler.sol"; 9 | 10 | /* solhint-disable state-visibility */ 11 | 12 | abstract contract BundlerTestSuite is IdGatewayTestSuite { 13 | KeyGateway keyGateway; 14 | Bundler bundler; 15 | 16 | function setUp() public virtual override { 17 | super.setUp(); 18 | 19 | keyGateway = new KeyGateway(address(keyRegistry), owner); 20 | 21 | vm.prank(owner); 22 | keyRegistry.setKeyGateway(address(keyGateway)); 23 | 24 | // Set up the BundleRegistry 25 | bundler = new Bundler( 26 | address(idGateway), 27 | address(keyGateway) 28 | ); 29 | 30 | addKnownContract(address(keyGateway)); 31 | addKnownContract(address(bundler)); 32 | } 33 | 34 | // Assert that a given fname was correctly registered with id 1 and recovery 35 | function _assertSuccessfulRegistration(address account, address recovery) internal { 36 | assertEq(idRegistry.idOf(account), 1); 37 | assertEq(idRegistry.recoveryOf(1), recovery); 38 | } 39 | 40 | // Assert that a given fname was not registered and the contracts have no registrations 41 | function _assertUnsuccessfulRegistration(address account) internal { 42 | assertEq(idRegistry.idOf(account), 0); 43 | assertEq(idRegistry.recoveryOf(1), address(0)); 44 | } 45 | 46 | function _signAdd( 47 | uint256 pk, 48 | address owner, 49 | uint32 keyType, 50 | bytes memory key, 51 | uint8 metadataType, 52 | bytes memory metadata, 53 | uint256 deadline 54 | ) internal returns (bytes memory signature) { 55 | return _signAdd(pk, owner, keyType, key, metadataType, metadata, keyGateway.nonces(owner), deadline); 56 | } 57 | 58 | function _signAdd( 59 | uint256 pk, 60 | address owner, 61 | uint32 keyType, 62 | bytes memory key, 63 | uint8 metadataType, 64 | bytes memory metadata, 65 | uint256 nonce, 66 | uint256 deadline 67 | ) internal returns (bytes memory signature) { 68 | bytes32 digest = keyGateway.hashTypedDataV4( 69 | keccak256( 70 | abi.encode( 71 | keyGateway.ADD_TYPEHASH(), 72 | owner, 73 | keyType, 74 | keccak256(key), 75 | metadataType, 76 | keccak256(metadata), 77 | nonce, 78 | deadline 79 | ) 80 | ) 81 | ); 82 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, digest); 83 | signature = abi.encodePacked(r, s, v); 84 | assertEq(signature.length, 65); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/RecoveryProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | import {IIdRegistry} from "./interfaces/IIdRegistry.sol"; 5 | import {Ownable2Step} from "openzeppelin/contracts/access/Ownable2Step.sol"; 6 | 7 | /** 8 | * @title Farcaster RecoveryProxy 9 | * 10 | * @notice RecoveryProxy allows the recovery execution logic to be changed 11 | * without changing the recovery address. 12 | * 13 | * The proxy is set to the recovery address and it delegates 14 | * permissions to execute the recovery to its owner. The owner 15 | * can be changed at any time, for example from an EOA to a 2/3 16 | * multisig. This allows a recovery service operator to change the 17 | * recovery mechanisms in the future without requiring each user to 18 | * come online and execute a transaction. 19 | * 20 | * @custom:security-contact security@merklemanufactory.com 21 | */ 22 | contract RecoveryProxy is Ownable2Step { 23 | /*////////////////////////////////////////////////////////////// 24 | EVENTS 25 | //////////////////////////////////////////////////////////////*/ 26 | 27 | /** 28 | * @notice Emit an event when owner changes the IdRegistry 29 | * 30 | * @param oldIdRegistry The previous IIdRegistry 31 | * @param newIdRegistry The new IIdRegistry 32 | */ 33 | event SetIdRegistry(IIdRegistry oldIdRegistry, IIdRegistry newIdRegistry); 34 | 35 | /*////////////////////////////////////////////////////////////// 36 | IMMUTABLES 37 | //////////////////////////////////////////////////////////////*/ 38 | 39 | /** 40 | * @dev Address of the IdRegistry contract 41 | */ 42 | IIdRegistry public idRegistry; 43 | 44 | /*////////////////////////////////////////////////////////////// 45 | CONSTRUCTOR 46 | //////////////////////////////////////////////////////////////*/ 47 | 48 | /** 49 | * @notice Configure the address of the IdRegistry contract and 50 | * set the initial owner. 51 | * 52 | * @param _idRegistry Address of the IdRegistry contract 53 | * @param _initialOwner Initial owner address 54 | */ 55 | constructor(address _idRegistry, address _initialOwner) { 56 | idRegistry = IIdRegistry(_idRegistry); 57 | _transferOwnership(_initialOwner); 58 | } 59 | 60 | /** 61 | * @notice Recover an fid for a user who has set the RecoveryProxy as their recovery address. 62 | * Only owner. 63 | * 64 | * @param from The address that currently owns the fid. 65 | * @param to The address to transfer the fid to. 66 | * @param deadline Expiration timestamp of the signature. 67 | * @param sig EIP-712 Transfer signature signed by the to address. 68 | */ 69 | function recover(address from, address to, uint256 deadline, bytes calldata sig) external onlyOwner { 70 | idRegistry.recover(from, to, deadline, sig); 71 | } 72 | 73 | /** 74 | * @notice Set the IdRegistry address. 75 | * Only owner. 76 | * 77 | * @param _idRegistry IDRegistry contract address. 78 | */ 79 | function setIdRegistry(IIdRegistry _idRegistry) external onlyOwner { 80 | emit SetIdRegistry(idRegistry, _idRegistry); 81 | idRegistry = _idRegistry; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/StorageRegistry/StorageRegistryTestSuite.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {StorageRegistryHarness, MockPriceFeed, MockUptimeFeed, MockChainlinkFeed, RevertOnReceive} from "../Utils.sol"; 5 | import {TestSuiteSetup} from "../TestSuiteSetup.sol"; 6 | 7 | /* solhint-disable state-visibility */ 8 | 9 | abstract contract StorageRegistryTestSuite is TestSuiteSetup { 10 | StorageRegistryHarness internal storageRegistry; 11 | MockPriceFeed internal priceFeed; 12 | MockUptimeFeed internal uptimeFeed; 13 | RevertOnReceive internal revertOnReceive; 14 | 15 | /*////////////////////////////////////////////////////////////// 16 | CONSTANTS 17 | //////////////////////////////////////////////////////////////*/ 18 | 19 | address internal deployer = address(this); 20 | address internal mallory = makeAddr("mallory"); 21 | address internal vault = makeAddr("vault"); 22 | address internal roleAdmin = makeAddr("roleAdmin"); 23 | address internal operator = makeAddr("operator"); 24 | address internal treasurer = makeAddr("treasurer"); 25 | 26 | uint256 internal immutable DEPLOYED_AT = block.timestamp + 3600; 27 | 28 | uint256 internal constant INITIAL_RENTAL_PERIOD = 365 days; 29 | uint256 internal constant INITIAL_USD_UNIT_PRICE = 5e8; // $5 USD 30 | uint256 internal constant INITIAL_MAX_UNITS = 2_000_000; 31 | 32 | int256 internal constant SEQUENCER_UP = 0; 33 | int256 internal constant ETH_USD_PRICE = 2000e8; // $2000 USD/ETH 34 | 35 | uint256 internal constant INITIAL_PRICE_FEED_CACHE_DURATION = 1 days; 36 | uint256 internal constant INITIAL_PRICE_FEED_MAX_AGE = 2 hours; 37 | uint256 internal constant INITIAL_UPTIME_FEED_GRACE_PERIOD = 1 hours; 38 | uint256 internal constant INITIAL_PRICE_IN_ETH = 0.0025 ether; 39 | uint256 internal constant INITIAL_PRICE_FEED_MIN_ANSWER = 100e8; // $100 USD 40 | uint256 internal constant INITIAL_PRICE_FEED_MAX_ANSWER = 10_000e8; // $10k USD 41 | 42 | function setUp() public virtual override { 43 | super.setUp(); 44 | 45 | priceFeed = new MockPriceFeed(); 46 | uptimeFeed = new MockUptimeFeed(); 47 | revertOnReceive = new RevertOnReceive(); 48 | 49 | uptimeFeed.setRoundData( 50 | MockChainlinkFeed.RoundData({ 51 | roundId: 1, 52 | answer: SEQUENCER_UP, 53 | startedAt: 0, 54 | timeStamp: block.timestamp, 55 | answeredInRound: 1 56 | }) 57 | ); 58 | 59 | priceFeed.setRoundData( 60 | MockChainlinkFeed.RoundData({ 61 | roundId: 1, 62 | answer: ETH_USD_PRICE, 63 | startedAt: 0, 64 | timeStamp: block.timestamp, 65 | answeredInRound: 1 66 | }) 67 | ); 68 | 69 | vm.warp(DEPLOYED_AT); 70 | 71 | storageRegistry = new StorageRegistryHarness( 72 | priceFeed, 73 | uptimeFeed, 74 | INITIAL_USD_UNIT_PRICE, 75 | INITIAL_MAX_UNITS, 76 | vault, 77 | roleAdmin, 78 | owner, 79 | operator, 80 | treasurer 81 | ); 82 | 83 | addKnownContract(address(priceFeed)); 84 | addKnownContract(address(uptimeFeed)); 85 | addKnownContract(address(revertOnReceive)); 86 | addKnownContract(address(storageRegistry)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/interfaces/IBundler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import {IIdGateway} from "./IIdGateway.sol"; 5 | import {IKeyGateway} from "./IKeyGateway.sol"; 6 | import {IStorageRegistry} from "./IStorageRegistry.sol"; 7 | 8 | interface IBundler { 9 | /*////////////////////////////////////////////////////////////// 10 | ERRORS 11 | //////////////////////////////////////////////////////////////*/ 12 | 13 | /// @dev Revert if the caller does not have the authority to perform the action. 14 | error Unauthorized(); 15 | 16 | /*////////////////////////////////////////////////////////////// 17 | STRUCTS 18 | //////////////////////////////////////////////////////////////*/ 19 | 20 | /// @notice Data needed to register an fid with signature. 21 | struct RegistrationParams { 22 | address to; 23 | address recovery; 24 | uint256 deadline; 25 | bytes sig; 26 | } 27 | 28 | /// @notice Data needed to add a signer with signature. 29 | struct SignerParams { 30 | uint32 keyType; 31 | bytes key; 32 | uint8 metadataType; 33 | bytes metadata; 34 | uint256 deadline; 35 | bytes sig; 36 | } 37 | 38 | /*////////////////////////////////////////////////////////////// 39 | CONSTANTS 40 | //////////////////////////////////////////////////////////////*/ 41 | 42 | /** 43 | * @notice Contract version specified in the Farcaster protocol version scheme. 44 | */ 45 | function VERSION() external view returns (string memory); 46 | 47 | /** 48 | * @dev Address of the IdGateway contract 49 | */ 50 | function idGateway() external view returns (IIdGateway); 51 | 52 | /** 53 | * @dev Address of the KeyGateway contract 54 | */ 55 | function keyGateway() external view returns (IKeyGateway); 56 | 57 | /*////////////////////////////////////////////////////////////// 58 | VIEWS 59 | //////////////////////////////////////////////////////////////*/ 60 | 61 | /** 62 | * @notice Calculate the total price of a registration. 63 | * 64 | * @param extraStorage Number of additional storage units to rent. All registrations include 1 65 | * storage unit, but additional storage can be rented at registration time. 66 | * 67 | * @return Total price in wei. 68 | * 69 | */ 70 | function price(uint256 extraStorage) external view returns (uint256); 71 | 72 | /*////////////////////////////////////////////////////////////// 73 | REGISTRATION 74 | //////////////////////////////////////////////////////////////*/ 75 | 76 | /** 77 | * @notice Register an fid, add one or more signers, and rent storage in a single transaction. 78 | * 79 | * @param registerParams Struct containing register parameters: to, recovery, deadline, and signature. 80 | * @param signerParams Array of structs containing signer parameters: keyType, key, metadataType, 81 | * metadata, deadline, and signature. 82 | * @param extraStorage Number of additional storage units to rent. (fid registration includes 1 unit). 83 | * 84 | */ 85 | function register( 86 | RegistrationParams calldata registerParams, 87 | SignerParams[] calldata signerParams, 88 | uint256 extraStorage 89 | ) external payable returns (uint256 fid); 90 | } 91 | -------------------------------------------------------------------------------- /src/Bundler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | import {IBundler} from "./interfaces/IBundler.sol"; 5 | import {IIdGateway} from "./interfaces/IIdGateway.sol"; 6 | import {IKeyGateway} from "./interfaces/IKeyGateway.sol"; 7 | import {TransferHelper} from "./libraries/TransferHelper.sol"; 8 | 9 | /** 10 | * @title Farcaster Bundler 11 | * 12 | * @notice See https://github.com/farcasterxyz/contracts/blob/v3.1.0/docs/docs.md for an overview. 13 | * 14 | * @custom:security-contact security@merklemanufactory.com 15 | */ 16 | contract Bundler is IBundler { 17 | using TransferHelper for address; 18 | 19 | /*////////////////////////////////////////////////////////////// 20 | CONSTANTS 21 | //////////////////////////////////////////////////////////////*/ 22 | 23 | /** 24 | * @inheritdoc IBundler 25 | */ 26 | string public constant VERSION = "2023.11.15"; 27 | 28 | /*////////////////////////////////////////////////////////////// 29 | IMMUTABLES 30 | //////////////////////////////////////////////////////////////*/ 31 | 32 | /** 33 | * @inheritdoc IBundler 34 | */ 35 | IIdGateway public immutable idGateway; 36 | 37 | /** 38 | * @inheritdoc IBundler 39 | */ 40 | IKeyGateway public immutable keyGateway; 41 | 42 | /*////////////////////////////////////////////////////////////// 43 | CONSTRUCTOR 44 | //////////////////////////////////////////////////////////////*/ 45 | 46 | /** 47 | * @notice Configure the addresses of the IdGateway and KeyGateway contracts. 48 | * 49 | * @param _idGateway Address of the IdGateway contract 50 | * @param _keyGateway Address of the KeyGateway contract 51 | */ 52 | constructor(address _idGateway, address _keyGateway) { 53 | idGateway = IIdGateway(payable(_idGateway)); 54 | keyGateway = IKeyGateway(payable(_keyGateway)); 55 | } 56 | 57 | /** 58 | * @inheritdoc IBundler 59 | */ 60 | function price(uint256 extraStorage) external view returns (uint256) { 61 | return idGateway.price(extraStorage); 62 | } 63 | 64 | /** 65 | * @inheritdoc IBundler 66 | */ 67 | function register( 68 | RegistrationParams calldata registerParams, 69 | SignerParams[] calldata signerParams, 70 | uint256 extraStorage 71 | ) external payable returns (uint256) { 72 | (uint256 fid, uint256 overpayment) = idGateway.registerFor{value: msg.value}( 73 | registerParams.to, registerParams.recovery, registerParams.deadline, registerParams.sig, extraStorage 74 | ); 75 | 76 | uint256 signersLen = signerParams.length; 77 | for (uint256 i; i < signersLen;) { 78 | SignerParams calldata signer = signerParams[i]; 79 | keyGateway.addFor( 80 | registerParams.to, 81 | signer.keyType, 82 | signer.key, 83 | signer.metadataType, 84 | signer.metadata, 85 | signer.deadline, 86 | signer.sig 87 | ); 88 | 89 | // Safety: i can be incremented unchecked since it is bound by signerParams.length. 90 | unchecked { 91 | ++i; 92 | } 93 | } 94 | if (overpayment > 0) msg.sender.sendNative(overpayment); 95 | return fid; 96 | } 97 | 98 | receive() external payable { 99 | if (msg.sender != address(idGateway)) revert Unauthorized(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/interfaces/abstract/IMigration.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | interface IMigration { 5 | /*////////////////////////////////////////////////////////////// 6 | ERRORS 7 | //////////////////////////////////////////////////////////////*/ 8 | 9 | /// @dev Revert if the caller is not the migrator. 10 | error OnlyMigrator(); 11 | 12 | /// @dev Revert if the migrator calls a migration function after the grace period. 13 | error PermissionRevoked(); 14 | 15 | /// @dev Revert if the migrator calls migrate more than once. 16 | error AlreadyMigrated(); 17 | 18 | /*////////////////////////////////////////////////////////////// 19 | EVENTS 20 | //////////////////////////////////////////////////////////////*/ 21 | 22 | /** 23 | * @dev Emit an event when the admin calls migrate(). Used to migrate 24 | * Hubs from reading events from one contract to another. 25 | * 26 | * @param migratedAt The timestamp at which the migration occurred. 27 | */ 28 | event Migrated(uint256 indexed migratedAt); 29 | 30 | /** 31 | * @notice Emit an event when the owner changes the migrator address. 32 | * 33 | * @param oldMigrator The address of the previous migrator. 34 | * @param newMigrator The address of the new migrator. 35 | */ 36 | event SetMigrator(address oldMigrator, address newMigrator); 37 | 38 | /*////////////////////////////////////////////////////////////// 39 | IMMUTABLES 40 | //////////////////////////////////////////////////////////////*/ 41 | 42 | /** 43 | * @notice Period in seconds after migration during which admin can continue to call protected 44 | * migration functions. Admins can make corrections to the migrated data during the 45 | * grace period if necessary, but cannot make changes after it expires. 46 | */ 47 | function gracePeriod() external view returns (uint24); 48 | 49 | /*////////////////////////////////////////////////////////////// 50 | STORAGE 51 | //////////////////////////////////////////////////////////////*/ 52 | 53 | /** 54 | * @notice Migration admin address. 55 | */ 56 | function migrator() external view returns (address); 57 | 58 | /** 59 | * @notice Timestamp at which data is migrated. Hubs will cut over to use this contract as their 60 | * source of truth after this timestamp. 61 | */ 62 | function migratedAt() external view returns (uint40); 63 | 64 | /*////////////////////////////////////////////////////////////// 65 | VIEWS 66 | //////////////////////////////////////////////////////////////*/ 67 | 68 | /** 69 | * @notice Check if the contract has been migrated. 70 | * 71 | * @return true if the contract has been migrated, false otherwise. 72 | */ 73 | function isMigrated() external view returns (bool); 74 | 75 | /*////////////////////////////////////////////////////////////// 76 | PERMISSIONED ACTIONS 77 | //////////////////////////////////////////////////////////////*/ 78 | 79 | /** 80 | * @notice Set the time of the migration and emit an event. Hubs will watch this event and 81 | * cut over to use this contract as their source of truth after this timestamp. 82 | * Only callable by the migrator. 83 | */ 84 | function migrate() external; 85 | 86 | /** 87 | * @notice Set the migrator address. Only callable by owner. 88 | * 89 | * @param _migrator Migrator address. 90 | */ 91 | function setMigrator(address _migrator) external; 92 | } 93 | -------------------------------------------------------------------------------- /src/abstract/Migration.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | import {Guardians} from "../abstract/Guardians.sol"; 5 | import {IMigration} from "../interfaces/abstract/IMigration.sol"; 6 | 7 | abstract contract Migration is IMigration, Guardians { 8 | /*////////////////////////////////////////////////////////////// 9 | IMMUTABLES 10 | //////////////////////////////////////////////////////////////*/ 11 | 12 | /** 13 | * @inheritdoc IMigration 14 | */ 15 | uint24 public immutable gracePeriod; 16 | 17 | /*////////////////////////////////////////////////////////////// 18 | STORAGE 19 | //////////////////////////////////////////////////////////////*/ 20 | 21 | /** 22 | * @inheritdoc IMigration 23 | */ 24 | address public migrator; 25 | 26 | /** 27 | * @inheritdoc IMigration 28 | */ 29 | uint40 public migratedAt; 30 | 31 | /*////////////////////////////////////////////////////////////// 32 | MODIFIERS 33 | //////////////////////////////////////////////////////////////*/ 34 | 35 | /** 36 | * @notice Allow only the migrator to call the protected function. 37 | * Revoke permissions after the migration period. 38 | */ 39 | modifier onlyMigrator() { 40 | if (msg.sender != migrator) revert OnlyMigrator(); 41 | if (isMigrated() && block.timestamp > migratedAt + gracePeriod) { 42 | revert PermissionRevoked(); 43 | } 44 | _requirePaused(); 45 | _; 46 | } 47 | 48 | /*////////////////////////////////////////////////////////////// 49 | CONSTRUCTOR 50 | //////////////////////////////////////////////////////////////*/ 51 | 52 | /** 53 | * @notice Set the grace period and migrator address. 54 | * Pauses contract at deployment time. 55 | * 56 | * @param _gracePeriod Migration grace period in seconds. 57 | * @param _initialOwner Initial owner address. Set as migrator. 58 | */ 59 | constructor(uint24 _gracePeriod, address _migrator, address _initialOwner) Guardians(_initialOwner) { 60 | gracePeriod = _gracePeriod; 61 | migrator = _migrator; 62 | emit SetMigrator(address(0), _migrator); 63 | _pause(); 64 | } 65 | 66 | /*////////////////////////////////////////////////////////////// 67 | VIEWS 68 | //////////////////////////////////////////////////////////////*/ 69 | 70 | /** 71 | * @inheritdoc IMigration 72 | */ 73 | function isMigrated() public view returns (bool) { 74 | return migratedAt != 0; 75 | } 76 | 77 | /*////////////////////////////////////////////////////////////// 78 | MIGRATION 79 | //////////////////////////////////////////////////////////////*/ 80 | 81 | /** 82 | * @inheritdoc IMigration 83 | */ 84 | function migrate() external { 85 | if (msg.sender != migrator) revert OnlyMigrator(); 86 | if (isMigrated()) revert AlreadyMigrated(); 87 | _requirePaused(); 88 | migratedAt = uint40(block.timestamp); 89 | emit Migrated(migratedAt); 90 | } 91 | 92 | /*////////////////////////////////////////////////////////////// 93 | SET MIGRATOR 94 | //////////////////////////////////////////////////////////////*/ 95 | 96 | /** 97 | * @inheritdoc IMigration 98 | */ 99 | function setMigrator(address _migrator) public onlyOwner { 100 | if (isMigrated()) revert AlreadyMigrated(); 101 | _requirePaused(); 102 | emit SetMigrator(migrator, _migrator); 103 | migrator = _migrator; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/TestSuiteSetup.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {ERC1271WalletMock, ERC1271MaliciousMockForceRevert} from "./Utils.sol"; 6 | 7 | abstract contract TestSuiteSetup is Test { 8 | /*////////////////////////////////////////////////////////////// 9 | CONSTANTS 10 | //////////////////////////////////////////////////////////////*/ 11 | 12 | uint256 constant SECP_256K1_ORDER = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141; 13 | 14 | address constant ADMIN = address(0xa6a4daBC320300cd0D38F77A6688C6b4048f4682); 15 | 16 | // Known contracts that must not be made to call other contracts in tests 17 | address[] internal knownContracts = [ 18 | address(0xCe71065D4017F316EC606Fe4422e11eB2c47c246), // FuzzerDict 19 | address(0x4e59b44847b379578588920cA78FbF26c0B4956C), // CREATE2 Factory 20 | address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D), // Vm cheatcode address 21 | address(0x000000000000000000636F6e736F6c652e6c6f67), // console.sol 22 | address(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f), // Default test contract 23 | address(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496), // address(this) 24 | address(0x185a4dc360CE69bDCceE33b3784B0282f7961aea), // ??? 25 | address(0x2e234DAe75C793f67A35089C9d99245E1C58470b), // ??? 26 | address(0xEFc56627233b02eA95bAE7e19F648d7DcD5Bb132), // ??? 27 | address(0xf5a2fE45F4f1308502b1C136b9EF8af136141382) // ??? 28 | ]; 29 | 30 | address owner = makeAddr("owner"); 31 | address trustedCaller = makeAddr("trustedCaller"); 32 | address migrator = makeAddr("migrator"); 33 | 34 | // Address of known contracts, in a mapping for faster lookup when fuzzing 35 | mapping(address => bool) isKnownContract; 36 | 37 | /*////////////////////////////////////////////////////////////// 38 | CONSTRUCTOR 39 | //////////////////////////////////////////////////////////////*/ 40 | 41 | function setUp() public virtual { 42 | // Set up the known contracts map 43 | for (uint256 i = 0; i < knownContracts.length; i++) { 44 | isKnownContract[knownContracts[i]] = true; 45 | } 46 | } 47 | 48 | /*////////////////////////////////////////////////////////////// 49 | HELPERS 50 | //////////////////////////////////////////////////////////////*/ 51 | 52 | function addKnownContract(address contractAddress) public { 53 | isKnownContract[contractAddress] = true; 54 | } 55 | 56 | // Ensures that a fuzzed address input does not match a known contract address 57 | function _assumeClean(address a) internal { 58 | assumeNoPrecompiles(a); 59 | vm.assume(!isKnownContract[a]); 60 | vm.assume(a != ADMIN); 61 | vm.assume(a != address(0)); 62 | } 63 | 64 | function _boundPk(uint256 pk) internal view returns (uint256) { 65 | return bound(pk, 1, SECP_256K1_ORDER - 1); 66 | } 67 | 68 | function _boundDeadline(uint40 deadline) internal view returns (uint256) { 69 | return block.timestamp + uint256(bound(deadline, 0, type(uint40).max)); 70 | } 71 | 72 | function _createMockERC1271(address ownerAddress) 73 | internal 74 | returns (ERC1271WalletMock mockWallet, address mockWalletAddress) 75 | { 76 | mockWallet = new ERC1271WalletMock(ownerAddress); 77 | mockWalletAddress = address(mockWallet); 78 | } 79 | 80 | function _createMaliciousMockERC1271(address ownerAddress) 81 | internal 82 | returns (ERC1271MaliciousMockForceRevert mockWallet, address mockWalletAddress) 83 | { 84 | mockWallet = new ERC1271MaliciousMockForceRevert(ownerAddress); 85 | mockWalletAddress = address(mockWallet); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/IdRegistry/IdRegistryTestSuite.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {IdRegistry} from "../../src/IdRegistry.sol"; 5 | import {TestSuiteSetup} from "../TestSuiteSetup.sol"; 6 | 7 | /* solhint-disable state-visibility */ 8 | 9 | abstract contract IdRegistryTestSuite is TestSuiteSetup { 10 | IdRegistry idRegistry; 11 | 12 | function setUp() public virtual override { 13 | super.setUp(); 14 | 15 | idRegistry = new IdRegistry(migrator, owner); 16 | 17 | vm.prank(owner); 18 | idRegistry.unpause(); 19 | 20 | addKnownContract(address(idRegistry)); 21 | } 22 | 23 | /*////////////////////////////////////////////////////////////// 24 | TEST HELPERS 25 | //////////////////////////////////////////////////////////////*/ 26 | 27 | function _register(address caller) internal returns (uint256 fid) { 28 | fid = _registerWithRecovery(caller, address(0)); 29 | } 30 | 31 | function _registerWithRecovery(address caller, address recovery) internal returns (uint256 fid) { 32 | vm.prank(idRegistry.idGateway()); 33 | fid = idRegistry.register(caller, recovery); 34 | } 35 | 36 | function _pause() public { 37 | vm.prank(owner); 38 | idRegistry.pause(); 39 | assertEq(idRegistry.paused(), true); 40 | } 41 | 42 | function _signTransfer( 43 | uint256 pk, 44 | uint256 fid, 45 | address to, 46 | uint256 deadline 47 | ) internal returns (bytes memory signature) { 48 | address signer = vm.addr(pk); 49 | bytes32 digest = idRegistry.hashTypedDataV4( 50 | keccak256(abi.encode(idRegistry.TRANSFER_TYPEHASH(), fid, to, idRegistry.nonces(signer), deadline)) 51 | ); 52 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, digest); 53 | signature = abi.encodePacked(r, s, v); 54 | assertEq(signature.length, 65); 55 | } 56 | 57 | function _signTransferAndChangeRecovery( 58 | uint256 pk, 59 | uint256 fid, 60 | address to, 61 | address recovery, 62 | uint256 deadline 63 | ) internal returns (bytes memory signature) { 64 | address signer = vm.addr(pk); 65 | bytes32 digest = idRegistry.hashTypedDataV4( 66 | keccak256( 67 | abi.encode( 68 | idRegistry.TRANSFER_AND_CHANGE_RECOVERY_TYPEHASH(), 69 | fid, 70 | to, 71 | recovery, 72 | idRegistry.nonces(signer), 73 | deadline 74 | ) 75 | ) 76 | ); 77 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, digest); 78 | signature = abi.encodePacked(r, s, v); 79 | assertEq(signature.length, 65); 80 | } 81 | 82 | function _signChangeRecoveryAddress( 83 | uint256 pk, 84 | uint256 fid, 85 | address from, 86 | address to, 87 | uint256 deadline 88 | ) internal returns (bytes memory signature) { 89 | address signer = vm.addr(pk); 90 | bytes32 digest = idRegistry.hashTypedDataV4( 91 | keccak256( 92 | abi.encode( 93 | idRegistry.CHANGE_RECOVERY_ADDRESS_TYPEHASH(), fid, from, to, idRegistry.nonces(signer), deadline 94 | ) 95 | ) 96 | ); 97 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, digest); 98 | signature = abi.encodePacked(r, s, v); 99 | assertEq(signature.length, 65); 100 | } 101 | 102 | function _signDigest(uint256 pk, bytes32 digest) internal returns (bytes memory signature) { 103 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, digest); 104 | signature = abi.encodePacked(r, s, v); 105 | assertEq(signature.length, 65); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contracts 2 | 3 | This repository contains all the contracts deployed by the [Farcaster protocol](https://github.com/farcasterxyz/protocol). The contracts are: 4 | 5 | 1. **[Id Registry](./src/IdRegistry.sol)** - tracks ownership of farcaster identities (fids). 6 | 2. **[Storage Registry](./src/StorageRegistry.sol)** - allocates storage to fids and collects rent. 7 | 3. **[Key Registry](./src/KeyRegistry.sol)** - tracks associations between fids and key pairs for signing messages. 8 | 4. **[Id Gateway](./src/IdGateway.sol)** - issues farcaster identities (fids) to new users. 9 | 5. **[Key Gateway](./src/KeyGateway.sol)** - adds new associations between fids and keys. 10 | 6. **[Bundler](./src/Bundler.sol)** - allows calling gateways and storage in a single transaction. 11 | 7. **[Signed Key Request Validator](./src/validators/SignedKeyRequestValidator.sol)** - validates key registry metadata. 12 | 8. **[Recovery Proxy](./src/RecoveryProxy.sol)** - proxy for recovery service operators to initiate fid recovery. 13 | 9. **[Fname Resolver](./src/FnameResolver.sol)** - validates Farcaster ENS names which were issued off-chain. 14 | 15 | Read the [docs](docs/docs.md) for more details on how the contracts work. 16 | 17 | ## Deployments 18 | 19 | The [v3.1 contracts](https://github.com/farcasterxyz/contracts/releases/tag/v3.1.0) are deployed across both OP Mainnet and Ethereum Mainnet. 20 | 21 | ### OP Mainnet 22 | 23 | | Contract | Address | 24 | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | 25 | | IdRegistry | [0x00000000fc6c5f01fc30151999387bb99a9f489b](https://optimistic.etherscan.io/address/0x00000000fc6c5f01fc30151999387bb99a9f489b) | 26 | | StorageRegistry | [0x00000000fcce7f938e7ae6d3c335bd6a1a7c593d](https://optimistic.etherscan.io/address/0x00000000fcce7f938e7ae6d3c335bd6a1a7c593d) | 27 | | KeyRegistry | [0x00000000fc1237824fb747abde0ff18990e59b7e](https://optimistic.etherscan.io/address/0x00000000fc1237824fb747abde0ff18990e59b7e) | 28 | | IdGateway | [0x00000000fc25870c6ed6b6c7e41fb078b7656f69](https://optimistic.etherscan.io/address/0x00000000fc25870c6ed6b6c7e41fb078b7656f69) | 29 | | KeyGateway | [0x00000000fc56947c7e7183f8ca4b62398caadf0b](https://optimistic.etherscan.io/address/0x00000000fc56947c7e7183f8ca4b62398caadf0b) | 30 | | Bundler | [0x00000000fc04c910a0b5fea33b03e0447ad0b0aa](https://optimistic.etherscan.io/address/0x00000000fc04c910a0b5fea33b03e0447ad0b0aa) | 31 | | SignedKeyRequestValidator | [0x00000000fc700472606ed4fa22623acf62c60553](https://optimistic.etherscan.io/address/0x00000000fc700472606ed4fa22623acf62c60553) | 32 | | RecoveryProxy | [0x00000000fcb080a4d6c39a9354da9eb9bc104cd7](https://optimistic.etherscan.io/address/0x00000000fcb080a4d6c39a9354da9eb9bc104cd7) | 33 | 34 | ### ETH Mainnet 35 | 36 | | Contract | Address | 37 | | ------------- | ---------------- | 38 | | FnameResolver | Not yet deployed | 39 | 40 | ## Audits 41 | 42 | The [v3.1 contracts](https://github.com/farcasterxyz/contracts/releases/tag/v3.1.0) contracts were reviewed by [0xMacro](https://0xmacro.com/) and [Cyfrin](https://www.cyfrin.io/). 43 | 44 | - [0xMacro Report A-3](https://0xmacro.com/library/audits/farcaster-3.html) 45 | - [Cyfrin Report](https://github.com/farcasterxyz/contracts/blob/fe24a79e8901e8f2479474b16e32f43b66455a1d/docs/audits/2023-11-05-cyfrin-farcaster-v1.0.pdf) 46 | 47 | The [v3.0 contracts](https://github.com/farcasterxyz/contracts/releases/tag/v3.0.0) contracts were reviewed by [0xMacro](https://0xmacro.com/): 48 | 49 | - [0xMacro Report A-1](https://0xmacro.com/library/audits/farcaster-1.html) 50 | - [0xMacro Report A-2](https://0xmacro.com/library/audits/farcaster-2.html) 51 | 52 | ## Contributing 53 | 54 | Please see the [contributing guidelines](CONTRIBUTING.md). 55 | -------------------------------------------------------------------------------- /test/abstract/Guardians/Guardians.symbolic.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {SymTest} from "halmos-cheatcodes/SymTest.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | 7 | import {Guardians} from "../../../src/abstract/Guardians.sol"; 8 | 9 | contract GuardiansExample is Guardians { 10 | constructor(address owner) Guardians(owner) {} 11 | } 12 | 13 | contract GuardiansSymTest is SymTest, Test { 14 | GuardiansExample guarded; 15 | address owner; 16 | address x; 17 | address y; 18 | 19 | function setUp() public { 20 | owner = address(0x1000); 21 | 22 | // Setup Guardians 23 | guarded = new GuardiansExample(owner); 24 | 25 | // Create symbolic addresses 26 | x = svm.createAddress("x"); 27 | y = svm.createAddress("y"); 28 | } 29 | 30 | function check_Invariants(bytes4 selector, address caller) public { 31 | _initState(); 32 | vm.assume(x != owner); 33 | vm.assume(x != y); 34 | 35 | // Record pre-state 36 | bool oldPaused = guarded.paused(); 37 | bool oldGuardianX = guarded.guardians(x); 38 | bool oldGuardianY = guarded.guardians(y); 39 | 40 | // Execute an arbitrary tx 41 | vm.prank(caller); 42 | (bool success,) = address(guarded).call(_calldataFor(selector)); 43 | vm.assume(success); // ignore reverting cases 44 | 45 | // Record post-state 46 | bool newPaused = guarded.paused(); 47 | bool newGuardianX = guarded.guardians(x); 48 | bool newGuardianY = guarded.guardians(y); 49 | 50 | // If the paused state is changed by any transaction... 51 | if (newPaused != oldPaused) { 52 | // If it wasn't paused before... 53 | if (!oldPaused) { 54 | // It must be paused now. 55 | assert(guarded.paused()); 56 | 57 | // The function called was pause(). 58 | assert(selector == guarded.pause.selector); 59 | 60 | // The caller must be the owner or a guardian. 61 | assert(caller == owner || guarded.guardians(caller)); 62 | } // Otherwise, if it *was* paused before... 63 | else { 64 | // It must be unpaused now. 65 | assert(!guarded.paused()); 66 | 67 | // The function called was unpause(). 68 | assert(selector == guarded.unpause.selector); 69 | 70 | // The caller must be the owner. 71 | assert(caller == owner); 72 | } 73 | } 74 | 75 | // If X's guardian state is changed by any transaction... 76 | if (newGuardianX != oldGuardianX) { 77 | // The caller must be the owner. 78 | assert(caller == owner); 79 | 80 | // Y's guardian state must not be changed. 81 | assert(newGuardianY == oldGuardianY); 82 | } 83 | 84 | // If Y's guardian state is changed by any transaction... 85 | if (newGuardianY != oldGuardianY) { 86 | // The caller must be the owner. 87 | assert(caller == owner); 88 | 89 | // X's guardian state must not be changed. 90 | assert(newGuardianX == oldGuardianX); 91 | } 92 | } 93 | 94 | /*////////////////////////////////////////////////////////////// 95 | HELPERS 96 | //////////////////////////////////////////////////////////////*/ 97 | 98 | /** 99 | * @dev Initialize IdRegistry with symbolic arguments for state. 100 | */ 101 | function _initState() public { 102 | if (svm.createBool("pause?")) { 103 | vm.prank(owner); 104 | guarded.pause(); 105 | } 106 | } 107 | 108 | /** 109 | * @dev Generates valid calldata for a given function selector. 110 | */ 111 | function _calldataFor(bytes4 selector) internal returns (bytes memory) { 112 | return abi.encodePacked(selector, svm.createBytes(1024, "data")); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/KeyRegistry/KeyRegistryTestHelpers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {KeyRegistry} from "../../src/KeyRegistry.sol"; 5 | 6 | library BulkAddDataBuilder { 7 | function empty() internal pure returns (KeyRegistry.BulkAddData[] memory) { 8 | return new KeyRegistry.BulkAddData[](0); 9 | } 10 | 11 | function addFid( 12 | KeyRegistry.BulkAddData[] memory addData, 13 | uint256 fid 14 | ) internal pure returns (KeyRegistry.BulkAddData[] memory) { 15 | KeyRegistry.BulkAddData[] memory newData = new KeyRegistry.BulkAddData[](addData.length + 1); 16 | for (uint256 i; i < addData.length; i++) { 17 | newData[i] = addData[i]; 18 | } 19 | newData[addData.length].fid = fid; 20 | return newData; 21 | } 22 | 23 | function addFidsWithKeys( 24 | KeyRegistry.BulkAddData[] memory addData, 25 | uint256[] memory fids, 26 | bytes[][] memory keys 27 | ) internal pure returns (KeyRegistry.BulkAddData[] memory) { 28 | for (uint256 i; i < fids.length; ++i) { 29 | addData = addFid(addData, fids[i]); 30 | bytes[] memory fidKeys = keys[i]; 31 | for (uint256 j; j < fidKeys.length; ++j) { 32 | addData = addKey(addData, i, fidKeys[j], bytes.concat("metadata-", fidKeys[j])); 33 | } 34 | } 35 | return addData; 36 | } 37 | 38 | function addKey( 39 | KeyRegistry.BulkAddData[] memory addData, 40 | uint256 index, 41 | bytes memory key, 42 | bytes memory metadata 43 | ) internal pure returns (KeyRegistry.BulkAddData[] memory) { 44 | KeyRegistry.BulkAddKey[] memory keys = addData[index].keys; 45 | KeyRegistry.BulkAddKey[] memory newKeys = new KeyRegistry.BulkAddKey[]( 46 | keys.length + 1 47 | ); 48 | 49 | for (uint256 i; i < keys.length; i++) { 50 | newKeys[i] = keys[i]; 51 | } 52 | newKeys[keys.length].key = key; 53 | newKeys[keys.length].metadata = metadata; 54 | addData[index].keys = newKeys; 55 | return addData; 56 | } 57 | } 58 | 59 | library BulkResetDataBuilder { 60 | function empty() internal pure returns (KeyRegistry.BulkResetData[] memory) { 61 | return new KeyRegistry.BulkResetData[](0); 62 | } 63 | 64 | function addFid( 65 | KeyRegistry.BulkResetData[] memory resetData, 66 | uint256 fid 67 | ) internal pure returns (KeyRegistry.BulkResetData[] memory) { 68 | KeyRegistry.BulkResetData[] memory newData = new KeyRegistry.BulkResetData[]( 69 | resetData.length + 1 70 | ); 71 | for (uint256 i; i < resetData.length; i++) { 72 | newData[i] = resetData[i]; 73 | } 74 | newData[resetData.length].fid = fid; 75 | return newData; 76 | } 77 | 78 | function addFidsWithKeys( 79 | KeyRegistry.BulkResetData[] memory resetData, 80 | uint256[] memory fids, 81 | bytes[][] memory keys 82 | ) internal pure returns (KeyRegistry.BulkResetData[] memory) { 83 | for (uint256 i; i < fids.length; ++i) { 84 | resetData = addFid(resetData, fids[i]); 85 | bytes[] memory fidKeys = keys[i]; 86 | for (uint256 j; j < fidKeys.length; ++j) { 87 | resetData = addKey(resetData, i, fidKeys[j]); 88 | } 89 | } 90 | return resetData; 91 | } 92 | 93 | function addKey( 94 | KeyRegistry.BulkResetData[] memory resetData, 95 | uint256 index, 96 | bytes memory key 97 | ) internal pure returns (KeyRegistry.BulkResetData[] memory) { 98 | bytes[] memory prevKeys = resetData[index].keys; 99 | bytes[] memory newKeys = new bytes[](prevKeys.length + 1); 100 | 101 | for (uint256 i; i < prevKeys.length; i++) { 102 | newKeys[i] = prevKeys[i]; 103 | } 104 | newKeys[prevKeys.length] = key; 105 | resetData[index].keys = newKeys; 106 | return resetData; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test/abstract/Migration/Migration.symbolic.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {SymTest} from "halmos-cheatcodes/SymTest.sol"; 5 | import {Test} from "forge-std/Test.sol"; 6 | 7 | import {Migration} from "../../../src/abstract/Migration.sol"; 8 | 9 | contract MigrationExample is Migration { 10 | constructor(uint256 gracePeriod, address migrator, address owner) Migration(uint24(gracePeriod), migrator, owner) {} 11 | 12 | function onlyCallableDuringMigration() external onlyMigrator {} 13 | } 14 | 15 | contract MigrationSymTest is SymTest, Test { 16 | MigrationExample migration; 17 | address migrator; 18 | address owner; 19 | uint256 gracePeriod; 20 | 21 | function setUp() public { 22 | owner = address(0x1000); 23 | migrator = address(0x2000); 24 | 25 | // Create symbolic gracePeriod 26 | gracePeriod = svm.createUint256("gracePeriod"); 27 | 28 | // Setup Migration 29 | migration = new MigrationExample(gracePeriod, migrator, owner); 30 | } 31 | 32 | function check_Invariants(bytes4 selector, address caller) public { 33 | _initState(); 34 | 35 | // Record pre-state 36 | uint40 oldMigratedAt = migration.migratedAt(); 37 | address oldMigrator = migration.migrator(); 38 | 39 | // Execute an arbitrary tx 40 | vm.prank(caller); 41 | (bool success,) = address(migration).call(_calldataFor(selector)); 42 | vm.assume(success); // ignore reverting cases 43 | 44 | // Record post-state 45 | uint40 newMigratedAt = migration.migratedAt(); 46 | address newMigrator = migration.migrator(); 47 | 48 | bool isPaused = migration.paused(); 49 | bool isMigrated = migration.isMigrated(); 50 | bool isInGracePeriod = block.timestamp <= migration.migratedAt() + migration.gracePeriod(); 51 | 52 | // If the migratedAt timestamp is changed by any transaction... 53 | if (newMigratedAt != oldMigratedAt) { 54 | // The previous value was zero. 55 | assert(oldMigratedAt == 0); 56 | 57 | // The function called was migrate(). 58 | assert(selector == migration.migrate.selector); 59 | 60 | // The caller must be the migrator. 61 | assert(caller == oldMigrator && oldMigrator == newMigrator); 62 | 63 | // The contract is paused. 64 | assert(isPaused); 65 | } 66 | 67 | // If the migrator address is changed by any transaction... 68 | if (newMigrator != oldMigrator) { 69 | // The function called was setMigrator(). 70 | assert(selector == migration.setMigrator.selector); 71 | 72 | // The caller must be the owner. 73 | assert(caller == owner); 74 | 75 | // The contract is unmigrated. 76 | assert(oldMigratedAt == 0 && oldMigratedAt == newMigratedAt); 77 | 78 | // The contract is paused. 79 | assert(isPaused); 80 | } 81 | 82 | // If the call was protected by a migration modifier... 83 | if (selector == migration.onlyCallableDuringMigration.selector) { 84 | // The state must be unchanged. 85 | assert(newMigratedAt == oldMigratedAt); 86 | 87 | // The caller must be the migrator. 88 | assert(caller == oldMigrator && oldMigrator == newMigrator); 89 | 90 | // The contract is unmigrated or in the grace period. 91 | assert(!isMigrated || isInGracePeriod); 92 | 93 | // The contract is paused. 94 | assert(isPaused); 95 | } 96 | } 97 | 98 | /*////////////////////////////////////////////////////////////// 99 | HELPERS 100 | //////////////////////////////////////////////////////////////*/ 101 | 102 | /** 103 | * @dev Initialize IdRegistry with symbolic arguments for state. 104 | */ 105 | function _initState() public { 106 | if (svm.createBool("isMigrated?")) { 107 | vm.prank(migration.migrator()); 108 | migration.migrate(); 109 | } 110 | } 111 | 112 | /** 113 | * @dev Generates valid calldata for a given function selector. 114 | */ 115 | function _calldataFor(bytes4 selector) internal returns (bytes memory) { 116 | return abi.encodePacked(selector, svm.createBytes(1024, "data")); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/libraries/EnumerableKeySet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | struct KeySet { 5 | // Storage of set values 6 | bytes[] _values; 7 | // Position of the value in the `values` array, plus 1 because index 0 8 | // means a value is not in the set. 9 | mapping(bytes => uint256) _indexes; 10 | } 11 | 12 | /** 13 | * @dev Modified from OpenZeppelin v4.9.3 EnumerableSet 14 | */ 15 | library EnumerableKeySet { 16 | /** 17 | * @dev Add a key to the set. O(1). 18 | * 19 | * Returns true if the value was added to the set, that is if it was not 20 | * already present. 21 | */ 22 | function add(KeySet storage set, bytes calldata value) internal returns (bool) { 23 | if (!contains(set, value)) { 24 | set._values.push(value); 25 | // The value is stored at length-1, but we add 1 to all indexes 26 | // and use 0 as a sentinel value 27 | set._indexes[value] = set._values.length; 28 | return true; 29 | } else { 30 | return false; 31 | } 32 | } 33 | 34 | /** 35 | * @dev Removes a key from the set. O(1). 36 | * 37 | * Returns true if the value was removed from the set, that is if it was 38 | * present. 39 | */ 40 | function remove(KeySet storage set, bytes calldata value) internal returns (bool) { 41 | // We read and store the value's index to prevent multiple reads from the same storage slot 42 | uint256 valueIndex = set._indexes[value]; 43 | 44 | if (valueIndex != 0) { 45 | // Equivalent to contains(set, value) 46 | // To delete an element from the _values array in O(1), we swap the element to delete with the last one in 47 | // the array, and then remove the last element (sometimes called as 'swap and pop'). 48 | // This modifies the order of the array. 49 | 50 | uint256 toDeleteIndex = valueIndex - 1; 51 | uint256 lastIndex = set._values.length - 1; 52 | 53 | if (lastIndex != toDeleteIndex) { 54 | bytes memory lastValue = set._values[lastIndex]; 55 | 56 | // Move the last value to the index where the value to delete is 57 | set._values[toDeleteIndex] = lastValue; 58 | // Update the index for the moved value 59 | set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex 60 | } 61 | 62 | // Delete the slot where the moved value was stored 63 | set._values.pop(); 64 | 65 | // Delete the index for the deleted slot 66 | delete set._indexes[value]; 67 | 68 | return true; 69 | } else { 70 | return false; 71 | } 72 | } 73 | 74 | /** 75 | * @dev Returns true if the value is in the set. O(1). 76 | */ 77 | function contains(KeySet storage set, bytes calldata value) internal view returns (bool) { 78 | return set._indexes[value] != 0; 79 | } 80 | 81 | /** 82 | * @dev Returns the number of values in the set. O(1). 83 | */ 84 | function length(KeySet storage set) internal view returns (uint256) { 85 | return set._values.length; 86 | } 87 | 88 | /** 89 | * @dev Returns the value stored at position `index` in the set. O(1). 90 | * 91 | * Note that there are no guarantees on the ordering of values inside the 92 | * array, and it may change when more values are added or removed. 93 | * 94 | * Requirements: 95 | * 96 | * - `index` must be strictly less than {length}. 97 | */ 98 | function at(KeySet storage set, uint256 index) internal view returns (bytes memory) { 99 | return set._values[index]; 100 | } 101 | 102 | /** 103 | * @dev Return the entire set in an array 104 | * 105 | * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed 106 | * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that 107 | * this function has an unbounded cost, and using it as part of a state-changing function may render the function 108 | * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. 109 | */ 110 | function values(KeySet storage set) internal view returns (bytes[] memory) { 111 | return set._values; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/KeyGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | import {IKeyGateway} from "./interfaces/IKeyGateway.sol"; 5 | import {IKeyRegistry} from "./interfaces/IKeyRegistry.sol"; 6 | import {EIP712} from "./abstract/EIP712.sol"; 7 | import {Nonces} from "./abstract/Nonces.sol"; 8 | import {Guardians} from "./abstract/Guardians.sol"; 9 | import {Signatures} from "./abstract/Signatures.sol"; 10 | 11 | /** 12 | * @title Farcaster KeyGateway 13 | * 14 | * @notice See https://github.com/farcasterxyz/contracts/blob/v3.1.0/docs/docs.md for an overview. 15 | * 16 | * @custom:security-contact security@merklemanufactory.com 17 | */ 18 | contract KeyGateway is IKeyGateway, Guardians, Signatures, EIP712, Nonces { 19 | /*////////////////////////////////////////////////////////////// 20 | CONSTANTS 21 | //////////////////////////////////////////////////////////////*/ 22 | 23 | /** 24 | * @inheritdoc IKeyGateway 25 | */ 26 | string public constant VERSION = "2023.11.15"; 27 | 28 | /** 29 | * @inheritdoc IKeyGateway 30 | */ 31 | bytes32 public constant ADD_TYPEHASH = keccak256( 32 | "Add(address owner,uint32 keyType,bytes key,uint8 metadataType,bytes metadata,uint256 nonce,uint256 deadline)" 33 | ); 34 | 35 | /*////////////////////////////////////////////////////////////// 36 | STORAGE 37 | //////////////////////////////////////////////////////////////*/ 38 | 39 | /** 40 | * @inheritdoc IKeyGateway 41 | */ 42 | IKeyRegistry public immutable keyRegistry; 43 | 44 | /*////////////////////////////////////////////////////////////// 45 | CONSTRUCTOR 46 | //////////////////////////////////////////////////////////////*/ 47 | 48 | /** 49 | * @notice Configure the address of the KeyRegistry contract. 50 | * Set the initial owner address. 51 | * 52 | * @param _keyRegistry Address of the KeyRegistry contract. 53 | * @param _initialOwner Address of the inital owner. 54 | */ 55 | constructor( 56 | address _keyRegistry, 57 | address _initialOwner 58 | ) Guardians(_initialOwner) EIP712("Farcaster KeyGateway", "1") { 59 | keyRegistry = IKeyRegistry(_keyRegistry); 60 | } 61 | 62 | /*////////////////////////////////////////////////////////////// 63 | REGISTRATION 64 | //////////////////////////////////////////////////////////////*/ 65 | 66 | /** 67 | * @inheritdoc IKeyGateway 68 | */ 69 | function add( 70 | uint32 keyType, 71 | bytes calldata key, 72 | uint8 metadataType, 73 | bytes calldata metadata 74 | ) external whenNotPaused { 75 | keyRegistry.add(msg.sender, keyType, key, metadataType, metadata); 76 | } 77 | 78 | /** 79 | * @inheritdoc IKeyGateway 80 | */ 81 | function addFor( 82 | address fidOwner, 83 | uint32 keyType, 84 | bytes calldata key, 85 | uint8 metadataType, 86 | bytes calldata metadata, 87 | uint256 deadline, 88 | bytes calldata sig 89 | ) external whenNotPaused { 90 | _verifyAddSig(fidOwner, keyType, key, metadataType, metadata, deadline, sig); 91 | keyRegistry.add(fidOwner, keyType, key, metadataType, metadata); 92 | } 93 | 94 | /*////////////////////////////////////////////////////////////// 95 | SIGNATURE VERIFICATION HELPERS 96 | //////////////////////////////////////////////////////////////*/ 97 | 98 | function _verifyAddSig( 99 | address fidOwner, 100 | uint32 keyType, 101 | bytes memory key, 102 | uint8 metadataType, 103 | bytes memory metadata, 104 | uint256 deadline, 105 | bytes memory sig 106 | ) internal { 107 | _verifySig( 108 | _hashTypedDataV4( 109 | keccak256( 110 | abi.encode( 111 | ADD_TYPEHASH, 112 | fidOwner, 113 | keyType, 114 | keccak256(key), 115 | metadataType, 116 | keccak256(metadata), 117 | _useNonce(fidOwner), 118 | deadline 119 | ) 120 | ) 121 | ), 122 | fidOwner, 123 | deadline, 124 | sig 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | x-build-defaults: &build-defaults 2 | build: 3 | dockerfile: Dockerfile.foundry 4 | context: . 5 | networks: 6 | - contracts_subnet 7 | 8 | x-anvil-defaults: &anvil-defaults 9 | <<: *build-defaults 10 | restart: on-failure 11 | healthcheck: 12 | test: ['CMD', '/usr/bin/nc', '-z', 'localhost', '${PORT:-8545}'] 13 | interval: 1s 14 | timeout: 1s 15 | retries: 3 16 | 17 | x-deployer-defaults: &deployer-defaults 18 | <<: *build-defaults 19 | environment: 20 | - DEPLOYER 21 | volumes: 22 | - .:/app 23 | working_dir: /app 24 | 25 | services: 26 | l2-anvil: 27 | <<: *anvil-defaults 28 | command: | 29 | sh -c ' 30 | exec anvil --host 0.0.0.0 --port ${PORT:-8545} --rpc-url $$L2_MAINNET_RPC_URL --state /var/lib/anvil/state --retries 3 --timeout 10000 31 | ' 32 | environment: 33 | - L2_MAINNET_RPC_URL 34 | volumes: 35 | - l2-anvil-data:/var/lib/anvil 36 | - l2-anvil-cache:/root/.foundry/cache 37 | ports: 38 | - '${PORT:-8545}:${PORT:-8545}' 39 | 40 | l2-deployer: 41 | <<: *deployer-defaults 42 | depends_on: 43 | - l2-anvil 44 | environment: 45 | - DEPLOYER 46 | - ID_REGISTRY_OWNER_ADDRESS 47 | - KEY_REGISTRY_OWNER_ADDRESS 48 | - BUNDLER_OWNER_ADDRESS 49 | - RECOVERY_PROXY_OWNER_ADDRESS 50 | - STORAGE_RENT_PRICE_FEED_ADDRESS 51 | - STORAGE_RENT_UPTIME_FEED_ADDRESS 52 | - STORAGE_RENT_VAULT_ADDRESS 53 | - STORAGE_RENT_ROLE_ADMIN_ADDRESS 54 | - STORAGE_RENT_ADMIN_ADDRESS 55 | - STORAGE_RENT_OPERATOR_ADDRESS 56 | - STORAGE_RENT_TREASURER_ADDRESS 57 | - BUNDLER_TRUSTED_CALLER_ADDRESS 58 | - METADATA_VALIDATOR_OWNER_ADDRESS 59 | - MIGRATOR_ADDRESS 60 | entrypoint: | 61 | sh -c ' 62 | set -e 63 | git config --global --add safe.directory "*" 64 | export RPC_URL="http://l2-anvil:${PORT:-8545}" 65 | echo "Waiting for Anvil..." 66 | while ! nc -z l2-anvil "${PORT:-8545}"; do sleep 0.1; done 67 | echo "Anvil online" 68 | echo "Enabling impersonation" 69 | cast rpc anvil_autoImpersonateAccount true --rpc-url "$$RPC_URL" > /dev/null 70 | echo "Funding deployer" 71 | cast rpc anvil_setBalance "$$DEPLOYER" 0xde0b6b3a7640000000 --rpc-url "$$RPC_URL" > /dev/null 72 | echo "Deploying contract" 73 | forge install 74 | forge script -v script/DeployL2.s.sol --rpc-url "$$RPC_URL" --unlocked --broadcast --sender "$$DEPLOYER" 75 | echo "Disabling impersonation" 76 | cast rpc anvil_autoImpersonateAccount false --rpc-url "$$RPC_URL" > /dev/null 77 | echo "Deploy complete" 78 | ' 79 | 80 | l1-anvil: 81 | <<: *anvil-defaults 82 | command: | 83 | sh -c ' 84 | exec anvil --host 0.0.0.0 --port ${PORT:-8545} --rpc-url $$L1_MAINNET_RPC_URL --state /var/lib/anvil/state --retries 3 --timeout 10000 85 | ' 86 | environment: 87 | - L1_MAINNET_RPC_URL 88 | volumes: 89 | - l1-anvil-data:/var/lib/anvil 90 | - l1-anvil-cache:/root/.foundry/cache 91 | ports: 92 | - '${PORT:-8546}:${PORT:-8545}' 93 | 94 | l1-deployer: 95 | <<: *deployer-defaults 96 | depends_on: 97 | - l1-anvil 98 | environment: 99 | - DEPLOYER 100 | - FNAME_RESOLVER_SERVER_URL 101 | - FNAME_RESOLVER_SIGNER_ADDRESS 102 | - FNAME_RESOLVER_OWNER_ADDRESS 103 | entrypoint: | 104 | sh -c ' 105 | set -e 106 | git config --global --add safe.directory "*" 107 | export RPC_URL="http://l1-anvil:${PORT:-8545}" 108 | echo "Waiting for Anvil..." 109 | while ! nc -z l1-anvil "${PORT:-8545}"; do sleep 0.1; done 110 | echo "Anvil online" 111 | echo "Enabling impersonation" 112 | cast rpc anvil_autoImpersonateAccount true --rpc-url "$$RPC_URL" > /dev/null 113 | echo "Funding deployer" 114 | cast rpc anvil_setBalance "$$DEPLOYER" 0xde0b6b3a7640000 --rpc-url "$$RPC_URL" > /dev/null 115 | echo "Deploying contract" 116 | forge install 117 | forge script -v script/DeployL1.s.sol --rpc-url "$$RPC_URL" --unlocked --broadcast --sender "$$DEPLOYER" 118 | echo "Disabling impersonation" 119 | cast rpc anvil_autoImpersonateAccount false --rpc-url "$$RPC_URL" > /dev/null 120 | echo "Deploy complete" 121 | ' 122 | 123 | volumes: 124 | l1-anvil-cache: 125 | l1-anvil-data: 126 | l2-anvil-cache: 127 | l2-anvil-data: 128 | 129 | networks: 130 | # Allows us to share the services in this file with other Docker Compose files 131 | contracts_subnet: 132 | driver: bridge 133 | -------------------------------------------------------------------------------- /test/RecoveryProxy/RecoveryProxy.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {IIdRegistry} from "../../src/interfaces/IIdRegistry.sol"; 5 | import {RecoveryProxyTestSuite} from "./RecoveryProxyTestSuite.sol"; 6 | 7 | /* solhint-disable state-visibility */ 8 | 9 | contract RecoveryProxyTest is RecoveryProxyTestSuite { 10 | event SetIdRegistry(address oldIdRegistry, address newIdRegistry); 11 | 12 | function testIdRegistry() public { 13 | assertEq(address(recoveryProxy.idRegistry()), address(idRegistry)); 14 | } 15 | 16 | function testInitialOwner() public { 17 | assertEq(recoveryProxy.owner(), owner); 18 | } 19 | 20 | function testFuzzRecoveryByProxy(address from, uint256 toPk, uint40 _deadline) public { 21 | toPk = _boundPk(toPk); 22 | address to = vm.addr(toPk); 23 | vm.assume(from != to); 24 | 25 | uint256 deadline = _boundDeadline(_deadline); 26 | uint256 fid = _registerWithRecovery(from, address(recoveryProxy)); 27 | bytes memory sig = _signTransfer(toPk, fid, to, deadline); 28 | 29 | assertEq(idRegistry.idOf(from), 1); 30 | assertEq(idRegistry.idOf(to), 0); 31 | assertEq(idRegistry.recoveryOf(1), address(recoveryProxy)); 32 | 33 | vm.prank(owner); 34 | recoveryProxy.recover(from, to, deadline, sig); 35 | 36 | assertEq(idRegistry.idOf(from), 0); 37 | assertEq(idRegistry.idOf(to), 1); 38 | assertEq(idRegistry.recoveryOf(1), address(recoveryProxy)); 39 | } 40 | 41 | function testFuzzRecoveryByProxyRevertsUnauthorized( 42 | address from, 43 | uint256 toPk, 44 | uint40 _deadline, 45 | address caller 46 | ) public { 47 | vm.assume(caller != owner); 48 | toPk = _boundPk(toPk); 49 | address to = vm.addr(toPk); 50 | vm.assume(from != to); 51 | 52 | uint256 deadline = _boundDeadline(_deadline); 53 | uint256 fid = _registerWithRecovery(from, address(recoveryProxy)); 54 | bytes memory sig = _signTransfer(toPk, fid, to, deadline); 55 | 56 | assertEq(idRegistry.idOf(from), 1); 57 | assertEq(idRegistry.idOf(to), 0); 58 | assertEq(idRegistry.recoveryOf(1), address(recoveryProxy)); 59 | 60 | vm.prank(caller); 61 | vm.expectRevert("Ownable: caller is not the owner"); 62 | recoveryProxy.recover(from, to, deadline, sig); 63 | 64 | assertEq(idRegistry.idOf(from), 1); 65 | assertEq(idRegistry.idOf(to), 0); 66 | assertEq(idRegistry.recoveryOf(1), address(recoveryProxy)); 67 | } 68 | 69 | function testFuzzChangeOwner(address from, uint256 toPk, uint40 _deadline, address newOwner) public { 70 | vm.assume(newOwner != owner); 71 | toPk = _boundPk(toPk); 72 | address to = vm.addr(toPk); 73 | vm.assume(from != to); 74 | 75 | uint256 deadline = _boundDeadline(_deadline); 76 | uint256 fid = _registerWithRecovery(from, address(recoveryProxy)); 77 | bytes memory sig = _signTransfer(toPk, fid, to, deadline); 78 | 79 | assertEq(idRegistry.idOf(from), 1); 80 | assertEq(idRegistry.idOf(to), 0); 81 | assertEq(idRegistry.recoveryOf(1), address(recoveryProxy)); 82 | 83 | vm.prank(owner); 84 | recoveryProxy.transferOwnership(newOwner); 85 | 86 | vm.prank(newOwner); 87 | recoveryProxy.acceptOwnership(); 88 | 89 | vm.prank(owner); 90 | vm.expectRevert("Ownable: caller is not the owner"); 91 | recoveryProxy.recover(from, to, deadline, sig); 92 | 93 | vm.prank(newOwner); 94 | recoveryProxy.recover(from, to, deadline, sig); 95 | 96 | assertEq(idRegistry.idOf(from), 0); 97 | assertEq(idRegistry.idOf(to), 1); 98 | assertEq(idRegistry.recoveryOf(1), address(recoveryProxy)); 99 | } 100 | 101 | function testFuzzOnlyOwnerCanSetIdRegistry(address caller, IIdRegistry _idRegistry) public { 102 | vm.assume(caller != owner); 103 | 104 | vm.prank(caller); 105 | vm.expectRevert("Ownable: caller is not the owner"); 106 | recoveryProxy.setIdRegistry(_idRegistry); 107 | } 108 | 109 | function testFuzzSetIdRegistry(IIdRegistry newIdRegistry) public { 110 | IIdRegistry currentIdRegistry = recoveryProxy.idRegistry(); 111 | 112 | vm.expectEmit(false, false, false, true); 113 | emit SetIdRegistry(address(currentIdRegistry), address(newIdRegistry)); 114 | 115 | vm.prank(owner); 116 | recoveryProxy.setIdRegistry(newIdRegistry); 117 | 118 | assertEq(address(recoveryProxy.idRegistry()), address(newIdRegistry)); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/IdGateway/IdGateway.owner.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {IdGateway} from "../../src/IdGateway.sol"; 5 | import {IGuardians} from "../../src/abstract/Guardians.sol"; 6 | import {IdGatewayTestSuite} from "./IdGatewayTestSuite.sol"; 7 | 8 | /* solhint-disable state-visibility */ 9 | 10 | contract IdGatewayOwnerTest is IdGatewayTestSuite { 11 | /*////////////////////////////////////////////////////////////// 12 | EVENTS 13 | //////////////////////////////////////////////////////////////*/ 14 | 15 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 16 | 17 | /*////////////////////////////////////////////////////////////// 18 | TRANSFER OWNERSHIP 19 | //////////////////////////////////////////////////////////////*/ 20 | 21 | function testFuzzTransferOwnership(address newOwner, address newOwner2) public { 22 | vm.assume(newOwner != address(0) && newOwner2 != address(0)); 23 | assertEq(idGateway.owner(), owner); 24 | assertEq(idGateway.pendingOwner(), address(0)); 25 | 26 | vm.prank(owner); 27 | idGateway.transferOwnership(newOwner); 28 | assertEq(idGateway.owner(), owner); 29 | assertEq(idGateway.pendingOwner(), newOwner); 30 | 31 | vm.prank(owner); 32 | idGateway.transferOwnership(newOwner2); 33 | assertEq(idGateway.owner(), owner); 34 | assertEq(idGateway.pendingOwner(), newOwner2); 35 | } 36 | 37 | function testFuzzCannotTransferOwnershipUnlessOwner(address alice, address newOwner) public { 38 | vm.assume(alice != owner && newOwner != address(0)); 39 | assertEq(idGateway.owner(), owner); 40 | assertEq(idGateway.pendingOwner(), address(0)); 41 | 42 | vm.prank(alice); 43 | vm.expectRevert("Ownable: caller is not the owner"); 44 | idGateway.transferOwnership(newOwner); 45 | 46 | assertEq(idGateway.owner(), owner); 47 | assertEq(idGateway.pendingOwner(), address(0)); 48 | } 49 | 50 | /*////////////////////////////////////////////////////////////// 51 | ACCEPT OWNERSHIP 52 | //////////////////////////////////////////////////////////////*/ 53 | 54 | function testFuzzAcceptOwnership(address newOwner) public { 55 | vm.assume(newOwner != owner && newOwner != address(0)); 56 | vm.prank(owner); 57 | idGateway.transferOwnership(newOwner); 58 | 59 | vm.expectEmit(); 60 | emit OwnershipTransferred(owner, newOwner); 61 | vm.prank(newOwner); 62 | idGateway.acceptOwnership(); 63 | 64 | assertEq(idGateway.owner(), newOwner); 65 | assertEq(idGateway.pendingOwner(), address(0)); 66 | } 67 | 68 | function testFuzzCannotAcceptOwnershipUnlessPendingOwner(address alice, address newOwner) public { 69 | vm.assume(alice != owner && alice != address(0)); 70 | vm.assume(newOwner != alice && newOwner != address(0)); 71 | 72 | vm.prank(owner); 73 | idGateway.transferOwnership(newOwner); 74 | 75 | vm.prank(alice); 76 | vm.expectRevert("Ownable2Step: caller is not the new owner"); 77 | idGateway.acceptOwnership(); 78 | 79 | assertEq(idGateway.owner(), owner); 80 | assertEq(idGateway.pendingOwner(), newOwner); 81 | } 82 | 83 | /*////////////////////////////////////////////////////////////// 84 | PAUSE 85 | //////////////////////////////////////////////////////////////*/ 86 | 87 | function testPause() public { 88 | assertEq(idGateway.owner(), owner); 89 | assertEq(idGateway.paused(), false); 90 | 91 | vm.prank(idGateway.owner()); 92 | idGateway.pause(); 93 | assertEq(idGateway.paused(), true); 94 | } 95 | 96 | function testFuzzCannotPauseUnlessGuardian(address alice) public { 97 | vm.assume(alice != owner && alice != address(0)); 98 | assertEq(idGateway.owner(), owner); 99 | assertEq(idGateway.paused(), false); 100 | 101 | vm.prank(alice); 102 | vm.expectRevert(IGuardians.OnlyGuardian.selector); 103 | idGateway.pause(); 104 | 105 | assertEq(idGateway.paused(), false); 106 | } 107 | 108 | function testUnpause() public { 109 | vm.prank(idGateway.owner()); 110 | idGateway.pause(); 111 | assertEq(idGateway.paused(), true); 112 | 113 | vm.prank(owner); 114 | idGateway.unpause(); 115 | 116 | assertEq(idGateway.paused(), false); 117 | } 118 | 119 | function testFuzzCannotUnpauseUnlessOwner(address alice) public { 120 | vm.assume(alice != owner && alice != address(0)); 121 | assertEq(idGateway.owner(), owner); 122 | 123 | vm.prank(idGateway.owner()); 124 | idGateway.pause(); 125 | assertEq(idGateway.paused(), true); 126 | 127 | vm.prank(alice); 128 | vm.expectRevert("Ownable: caller is not the owner"); 129 | idGateway.unpause(); 130 | 131 | assertEq(idGateway.paused(), true); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /script/LocalDeploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import "forge-std/Script.sol"; 5 | import "forge-std/console.sol"; 6 | import {AggregatorV3Interface} from "chainlink/v0.8/interfaces/AggregatorV3Interface.sol"; 7 | 8 | import {IdRegistry} from "../src/IdRegistry.sol"; 9 | import {StorageRegistry} from "../src/StorageRegistry.sol"; 10 | import {KeyRegistry} from "../src/KeyRegistry.sol"; 11 | import {MockPriceFeed, MockUptimeFeed, MockChainlinkFeed} from "../test/Utils.sol"; 12 | 13 | contract LocalDeploy is Script { 14 | uint256 internal constant INITIAL_RENTAL_PERIOD = 365 days; 15 | uint256 internal constant INITIAL_USD_UNIT_PRICE = 5e8; // $5 USD 16 | uint256 internal constant INITIAL_MAX_UNITS = 2_000_000; 17 | uint256 internal constant INITIAL_PRICE_FEED_CACHE_DURATION = 1 days; 18 | uint256 internal constant INITIAL_UPTIME_FEED_GRACE_PERIOD = 1 hours; 19 | 20 | bytes32 internal constant ID_REGISTRY_CREATE2_SALT = "fc"; 21 | bytes32 internal constant KEY_REGISTRY_CREATE2_SALT = "fc"; 22 | bytes32 internal constant STORAGE_RENT_CREATE2_SALT = "fc"; 23 | 24 | function run() public { 25 | _etchCreate2Deployer(); 26 | 27 | address initialIdRegistryOwner = vm.envAddress("ID_REGISTRY_OWNER_ADDRESS"); 28 | address initialKeyRegistryOwner = vm.envAddress("KEY_REGISTRY_OWNER_ADDRESS"); 29 | 30 | address vault = vm.envAddress("STORAGE_RENT_VAULT_ADDRESS"); 31 | address roleAdmin = vm.envAddress("STORAGE_RENT_ROLE_ADMIN_ADDRESS"); 32 | address admin = vm.envAddress("STORAGE_RENT_ADMIN_ADDRESS"); 33 | address operator = vm.envAddress("STORAGE_RENT_OPERATOR_ADDRESS"); 34 | address treasurer = vm.envAddress("STORAGE_RENT_TREASURER_ADDRESS"); 35 | address migrator = vm.envAddress("MIGRATOR_ADDRESS"); 36 | 37 | vm.startBroadcast(); 38 | (AggregatorV3Interface priceFeed, AggregatorV3Interface uptimeFeed) = _getOrDeployPriceFeeds(); 39 | IdRegistry idRegistry = new IdRegistry{salt: ID_REGISTRY_CREATE2_SALT}( 40 | migrator, 41 | initialIdRegistryOwner 42 | ); 43 | KeyRegistry keyRegistry = new KeyRegistry{ 44 | salt: KEY_REGISTRY_CREATE2_SALT 45 | }(address(idRegistry), migrator, initialKeyRegistryOwner, 1000); 46 | StorageRegistry storageRegistry = new StorageRegistry{ 47 | salt: STORAGE_RENT_CREATE2_SALT 48 | }( 49 | priceFeed, 50 | uptimeFeed, 51 | INITIAL_USD_UNIT_PRICE, 52 | INITIAL_MAX_UNITS, 53 | vault, 54 | roleAdmin, 55 | admin, 56 | operator, 57 | treasurer 58 | ); 59 | vm.stopBroadcast(); 60 | console.log("ID Registry: %s", address(idRegistry)); 61 | console.log("Key Registry: %s", address(keyRegistry)); 62 | console.log("Storage Rent: %s", address(storageRegistry)); 63 | } 64 | 65 | /* @dev Make an Anvil RPC call to deploy the same CREATE2 deployer Foundry uses on mainnet. */ 66 | function _etchCreate2Deployer() internal { 67 | if (block.chainid == 31337) { 68 | string[] memory command = new string[](5); 69 | command[0] = "cast"; 70 | command[1] = "rpc"; 71 | command[2] = "anvil_setCode"; 72 | command[3] = "0x4e59b44847b379578588920cA78FbF26c0B4956C"; 73 | command[4] = ( 74 | "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" 75 | "e03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3" 76 | ); 77 | vm.ffi(command); 78 | } 79 | } 80 | 81 | /* @dev Warp block.timestamp forward 3600 seconds, beyond the uptime feed grace period. */ 82 | function _warpForward() internal { 83 | if (block.chainid == 31337) { 84 | string[] memory command = new string[](4); 85 | command[0] = "cast"; 86 | command[1] = "rpc"; 87 | command[2] = "evm_increaseTime"; 88 | command[3] = "0xe10"; 89 | vm.ffi(command); 90 | } 91 | } 92 | 93 | /* @dev Deploy mock price feeds if we're on Anvil, otherwise read their addresses from the environment. */ 94 | function _getOrDeployPriceFeeds() 95 | internal 96 | returns (AggregatorV3Interface priceFeed, AggregatorV3Interface uptimeFeed) 97 | { 98 | if (block.chainid == 31337) { 99 | MockPriceFeed _priceFeed = new MockPriceFeed{salt: bytes32(0)}(); 100 | MockUptimeFeed _uptimeFeed = new MockUptimeFeed{salt: bytes32(0)}(); 101 | _priceFeed.setRoundData( 102 | MockChainlinkFeed.RoundData({ 103 | roundId: 1, 104 | answer: 2000e8, 105 | startedAt: 0, 106 | timeStamp: block.timestamp, 107 | answeredInRound: 1 108 | }) 109 | ); 110 | _uptimeFeed.setRoundData( 111 | MockChainlinkFeed.RoundData({ 112 | roundId: 1, 113 | answer: 0, 114 | startedAt: 0, 115 | timeStamp: block.timestamp, 116 | answeredInRound: 1 117 | }) 118 | ); 119 | _warpForward(); 120 | priceFeed = _priceFeed; 121 | uptimeFeed = _uptimeFeed; 122 | } else { 123 | priceFeed = AggregatorV3Interface(vm.envAddress("STORAGE_RENT_PRICE_FEED_ADDRESS")); 124 | uptimeFeed = AggregatorV3Interface(vm.envAddress("STORAGE_RENT_UPTIME_FEED_ADDRESS")); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /test/Utils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {AggregatorV3Interface} from "chainlink/v0.8/interfaces/AggregatorV3Interface.sol"; 5 | 6 | import {FnameResolver} from "../src/FnameResolver.sol"; 7 | import {IdRegistry} from "../src/IdRegistry.sol"; 8 | import {KeyRegistry} from "../src/KeyRegistry.sol"; 9 | import {StorageRegistry} from "../src/StorageRegistry.sol"; 10 | import {SignedKeyRequestValidator} from "../src/validators/SignedKeyRequestValidator.sol"; 11 | import {Bundler} from "../src/Bundler.sol"; 12 | import {Ownable} from "openzeppelin/contracts/access/Ownable.sol"; 13 | import {IERC1271} from "openzeppelin/contracts/interfaces/IERC1271.sol"; 14 | import {SignatureChecker} from "openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; 15 | 16 | /* solhint-disable no-empty-blocks */ 17 | 18 | contract StorageRegistryHarness is StorageRegistry { 19 | constructor( 20 | AggregatorV3Interface _priceFeed, 21 | AggregatorV3Interface _uptimeFeed, 22 | uint256 _usdUnitPrice, 23 | uint256 _maxUnits, 24 | address _vault, 25 | address _roleAdmin, 26 | address _owner, 27 | address _operator, 28 | address _treasurer 29 | ) 30 | StorageRegistry( 31 | _priceFeed, 32 | _uptimeFeed, 33 | _usdUnitPrice, 34 | _maxUnits, 35 | _vault, 36 | _roleAdmin, 37 | _owner, 38 | _operator, 39 | _treasurer 40 | ) 41 | {} 42 | 43 | function ownerRoleId() external pure returns (bytes32) { 44 | return OWNER_ROLE; 45 | } 46 | 47 | function operatorRoleId() external pure returns (bytes32) { 48 | return OPERATOR_ROLE; 49 | } 50 | 51 | function treasurerRoleId() external pure returns (bytes32) { 52 | return TREASURER_ROLE; 53 | } 54 | } 55 | 56 | contract StubValidator { 57 | bool isValid = true; 58 | 59 | function validate( 60 | uint256, /* userFid */ 61 | bytes memory, /* signerPubKey */ 62 | bytes memory /* appIdBytes */ 63 | ) external view returns (bool) { 64 | return isValid; 65 | } 66 | 67 | function setIsValid(bool val) external { 68 | isValid = val; 69 | } 70 | } 71 | 72 | contract MockChainlinkFeed is AggregatorV3Interface { 73 | struct RoundData { 74 | uint80 roundId; 75 | int256 answer; 76 | uint256 startedAt; 77 | uint256 timeStamp; 78 | uint80 answeredInRound; 79 | } 80 | 81 | RoundData public roundData; 82 | 83 | uint8 public decimals; 84 | string public description; 85 | uint256 public version = 1; 86 | 87 | bool public shouldRevert; 88 | bool public stubTimeStamp; 89 | 90 | constructor(uint8 _decimals, string memory _description) { 91 | decimals = _decimals; 92 | description = _description; 93 | } 94 | 95 | function setShouldRevert(bool _shouldRevert) external { 96 | shouldRevert = _shouldRevert; 97 | } 98 | 99 | function setStubTimeStamp(bool _stubTimeStamp) external { 100 | stubTimeStamp = _stubTimeStamp; 101 | } 102 | 103 | function setAnswer(int256 value) external { 104 | roundData.answer = value; 105 | } 106 | 107 | function setRoundData(RoundData calldata _roundData) external { 108 | roundData = _roundData; 109 | } 110 | 111 | function getRoundData(uint80) external view returns (uint80, int256, uint256, uint256, uint80) { 112 | return latestRoundData(); 113 | } 114 | 115 | function latestRoundData() public view returns (uint80, int256, uint256, uint256, uint80) { 116 | if (shouldRevert) revert("MockChainLinkFeed: Call failed"); 117 | return ( 118 | roundData.roundId, 119 | roundData.answer, 120 | roundData.startedAt, 121 | stubTimeStamp ? roundData.timeStamp : block.timestamp, 122 | roundData.answeredInRound 123 | ); 124 | } 125 | } 126 | 127 | contract MockPriceFeed is MockChainlinkFeed(8, "Mock ETH/USD Price Feed") { 128 | function setPrice(int256 _price) external { 129 | roundData.answer = _price; 130 | } 131 | } 132 | 133 | contract MockUptimeFeed is MockChainlinkFeed(0, "Mock L2 Sequencer Uptime Feed") {} 134 | 135 | contract RevertOnReceive { 136 | receive() external payable { 137 | revert("Cannot receive ETH"); 138 | } 139 | } 140 | 141 | /*////////////////////////////////////////////////////////////// 142 | SMART CONTRACT WALLET MOCKS 143 | //////////////////////////////////////////////////////////////*/ 144 | 145 | contract ERC1271WalletMock is Ownable, IERC1271 { 146 | constructor(address owner) { 147 | super.transferOwnership(owner); 148 | } 149 | 150 | function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue) { 151 | return 152 | SignatureChecker.isValidSignatureNow(owner(), hash, signature) ? this.isValidSignature.selector : bytes4(0); 153 | } 154 | } 155 | 156 | contract ERC1271MaliciousMockForceRevert is Ownable, IERC1271 { 157 | bool internal _forceRevert = true; 158 | 159 | constructor(address owner) { 160 | super.transferOwnership(owner); 161 | } 162 | 163 | function setForceRevert(bool forceRevert) external { 164 | _forceRevert = forceRevert; 165 | } 166 | 167 | function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4) { 168 | if (_forceRevert) { 169 | assembly { 170 | mstore(0, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) 171 | return(0, 32) 172 | } 173 | } 174 | 175 | return 176 | SignatureChecker.isValidSignatureNow(owner(), hash, signature) ? this.isValidSignature.selector : bytes4(0); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/interfaces/IIdGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import {IStorageRegistry} from "./IStorageRegistry.sol"; 5 | import {IIdRegistry} from "./IIdRegistry.sol"; 6 | 7 | interface IIdGateway { 8 | /*////////////////////////////////////////////////////////////// 9 | ERRORS 10 | //////////////////////////////////////////////////////////////*/ 11 | 12 | /// @dev Revert if the caller does not have the authority to perform the action. 13 | error Unauthorized(); 14 | 15 | /*////////////////////////////////////////////////////////////// 16 | EVENTS 17 | //////////////////////////////////////////////////////////////*/ 18 | 19 | /** 20 | * @dev Emit an event when the admin sets a new StorageRegistry address. 21 | * 22 | * @param oldStorageRegistry The previous StorageRegistry address. 23 | * @param newStorageRegistry The new StorageRegistry address. 24 | */ 25 | event SetStorageRegistry(address oldStorageRegistry, address newStorageRegistry); 26 | 27 | /*////////////////////////////////////////////////////////////// 28 | CONSTANTS 29 | //////////////////////////////////////////////////////////////*/ 30 | 31 | /** 32 | * @notice Contract version specified in the Farcaster protocol version scheme. 33 | */ 34 | function VERSION() external view returns (string memory); 35 | 36 | /** 37 | * @notice EIP-712 typehash for Register signatures. 38 | */ 39 | function REGISTER_TYPEHASH() external view returns (bytes32); 40 | 41 | /*////////////////////////////////////////////////////////////// 42 | STORAGE 43 | //////////////////////////////////////////////////////////////*/ 44 | 45 | /** 46 | * @notice The IdRegistry contract 47 | */ 48 | function idRegistry() external view returns (IIdRegistry); 49 | 50 | /** 51 | * @notice The StorageRegistry contract 52 | */ 53 | function storageRegistry() external view returns (IStorageRegistry); 54 | 55 | /*////////////////////////////////////////////////////////////// 56 | PRICE VIEW 57 | //////////////////////////////////////////////////////////////*/ 58 | 59 | /** 60 | * @notice Calculate the total price to register, equal to 1 storage unit. 61 | * 62 | * @return Total price in wei. 63 | */ 64 | function price() external view returns (uint256); 65 | 66 | /** 67 | * @notice Calculate the total price to register, including additional storage. 68 | * 69 | * @param extraStorage Number of additional storage units to rent. 70 | * 71 | * @return Total price in wei. 72 | */ 73 | function price(uint256 extraStorage) external view returns (uint256); 74 | 75 | /*////////////////////////////////////////////////////////////// 76 | REGISTRATION LOGIC 77 | //////////////////////////////////////////////////////////////*/ 78 | 79 | /** 80 | * @notice Register a new Farcaster ID (fid) to the caller. The caller must not have an fid. 81 | * 82 | * @param recovery Address which can recover the fid. Set to zero to disable recovery. 83 | * 84 | * @return fid registered FID. 85 | */ 86 | function register(address recovery) external payable returns (uint256 fid, uint256 overpayment); 87 | 88 | /** 89 | * @notice Register a new Farcaster ID (fid) to the caller and rent additional storage. 90 | * The caller must not have an fid. 91 | * 92 | * @param recovery Address which can recover the fid. Set to zero to disable recovery. 93 | * @param extraStorage Number of additional storage units to rent. 94 | * 95 | * @return fid registered FID. 96 | */ 97 | function register( 98 | address recovery, 99 | uint256 extraStorage 100 | ) external payable returns (uint256 fid, uint256 overpayment); 101 | 102 | /** 103 | * @notice Register a new Farcaster ID (fid) to any address. A signed message from the address 104 | * must be provided which approves both the to and the recovery. The address must not 105 | * have an fid. 106 | * 107 | * @param to Address which will own the fid. 108 | * @param recovery Address which can recover the fid. Set to zero to disable recovery. 109 | * @param deadline Expiration timestamp of the signature. 110 | * @param sig EIP-712 Register signature signed by the to address. 111 | * 112 | * @return fid registered FID. 113 | */ 114 | function registerFor( 115 | address to, 116 | address recovery, 117 | uint256 deadline, 118 | bytes calldata sig 119 | ) external payable returns (uint256 fid, uint256 overpayment); 120 | 121 | /** 122 | * @notice Register a new Farcaster ID (fid) to any address and rent additional storage. 123 | * A signed message from the address must be provided which approves both the to 124 | * and the recovery. The address must not have an fid. 125 | * 126 | * @param to Address which will own the fid. 127 | * @param recovery Address which can recover the fid. Set to zero to disable recovery. 128 | * @param deadline Expiration timestamp of the signature. 129 | * @param sig EIP-712 Register signature signed by the to address. 130 | * @param extraStorage Number of additional storage units to rent. 131 | * 132 | * @return fid registered FID. 133 | */ 134 | function registerFor( 135 | address to, 136 | address recovery, 137 | uint256 deadline, 138 | bytes calldata sig, 139 | uint256 extraStorage 140 | ) external payable returns (uint256 fid, uint256 overpayment); 141 | 142 | /*////////////////////////////////////////////////////////////// 143 | PERMISSIONED ACTIONS 144 | //////////////////////////////////////////////////////////////*/ 145 | 146 | /** 147 | * @notice Set the StorageRegistry address. Only callable by owner. 148 | * 149 | * @param _storageRegistry The new StorageREgistry address. 150 | */ 151 | function setStorageRegistry(address _storageRegistry) external; 152 | } 153 | -------------------------------------------------------------------------------- /src/validators/SignedKeyRequestValidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | import {Ownable2Step} from "openzeppelin/contracts/access/Ownable2Step.sol"; 5 | 6 | import {EIP712} from "../abstract/EIP712.sol"; 7 | import {IMetadataValidator} from "../interfaces/IMetadataValidator.sol"; 8 | import {IdRegistryLike} from "../interfaces/IdRegistryLike.sol"; 9 | 10 | /** 11 | * @title Farcaster SignedKeyRequestValidator 12 | * 13 | * @notice See https://github.com/farcasterxyz/contracts/blob/v3.1.0/docs/docs.md for an overview. 14 | * 15 | * @custom:security-contact security@merklemanufactory.com 16 | */ 17 | contract SignedKeyRequestValidator is IMetadataValidator, Ownable2Step, EIP712 { 18 | /*////////////////////////////////////////////////////////////// 19 | STRUCTS 20 | //////////////////////////////////////////////////////////////*/ 21 | 22 | /** 23 | * @notice Signed key request specific metadata. 24 | * 25 | * @param requestFid The fid of the entity requesting to add 26 | * a signer key. 27 | * @param requestSigner Signer address. Must be the owner of 28 | * requestFid. 29 | * @param signature EIP-712 SignedKeyRequest signature. 30 | * @param deadline block.timestamp after which signature expires. 31 | */ 32 | struct SignedKeyRequestMetadata { 33 | uint256 requestFid; 34 | address requestSigner; 35 | bytes signature; 36 | uint256 deadline; 37 | } 38 | 39 | /*////////////////////////////////////////////////////////////// 40 | EVENTS 41 | //////////////////////////////////////////////////////////////*/ 42 | 43 | /** 44 | * @dev Emit an event when the admin sets a new IdRegistry contract address. 45 | * 46 | * @param oldIdRegistry The previous IdRegistry address. 47 | * @param newIdRegistry The new IdRegistry address. 48 | */ 49 | event SetIdRegistry(address oldIdRegistry, address newIdRegistry); 50 | 51 | /*////////////////////////////////////////////////////////////// 52 | CONSTANTS 53 | //////////////////////////////////////////////////////////////*/ 54 | 55 | /** 56 | * @dev Contract version specified using Farcaster protocol version scheme. 57 | */ 58 | string public constant VERSION = "2023.08.23"; 59 | 60 | bytes32 public constant METADATA_TYPEHASH = 61 | keccak256("SignedKeyRequest(uint256 requestFid,bytes key,uint256 deadline)"); 62 | 63 | /*////////////////////////////////////////////////////////////// 64 | STORAGE 65 | //////////////////////////////////////////////////////////////*/ 66 | 67 | /** 68 | * @dev The IdRegistry contract. 69 | */ 70 | IdRegistryLike public idRegistry; 71 | 72 | /*////////////////////////////////////////////////////////////// 73 | CONSTRUCTOR 74 | //////////////////////////////////////////////////////////////*/ 75 | 76 | /** 77 | * @notice Set the IdRegistry and owner. 78 | * 79 | * @param _idRegistry IdRegistry contract address. 80 | * @param _initialOwner Initial contract owner address. 81 | */ 82 | constructor(address _idRegistry, address _initialOwner) EIP712("Farcaster SignedKeyRequestValidator", "1") { 83 | idRegistry = IdRegistryLike(_idRegistry); 84 | _transferOwnership(_initialOwner); 85 | } 86 | 87 | /*////////////////////////////////////////////////////////////// 88 | VALIDATION 89 | //////////////////////////////////////////////////////////////*/ 90 | 91 | /** 92 | * @notice Validate the SignedKeyRequest metadata associated with a signer key. 93 | * (Key type 1, Metadata type 1) 94 | * 95 | * @param key The EdDSA public key of the signer. 96 | * @param signedKeyRequestBytes An abi-encoded SignedKeyRequest struct, provided as the 97 | * metadata argument to KeyRegistry.add. 98 | * 99 | * @return true if signature is valid and signer owns requestFid, false otherwise. 100 | */ 101 | function validate( 102 | uint256, /* userFid */ 103 | bytes memory key, 104 | bytes calldata signedKeyRequestBytes 105 | ) external view returns (bool) { 106 | SignedKeyRequestMetadata memory metadata = abi.decode(signedKeyRequestBytes, (SignedKeyRequestMetadata)); 107 | 108 | if (idRegistry.idOf(metadata.requestSigner) != metadata.requestFid) { 109 | return false; 110 | } 111 | if (block.timestamp > metadata.deadline) return false; 112 | if (key.length != 32) return false; 113 | 114 | return idRegistry.verifyFidSignature( 115 | metadata.requestSigner, 116 | metadata.requestFid, 117 | _hashTypedDataV4( 118 | keccak256(abi.encode(METADATA_TYPEHASH, metadata.requestFid, keccak256(key), metadata.deadline)) 119 | ), 120 | metadata.signature 121 | ); 122 | } 123 | 124 | /*////////////////////////////////////////////////////////////// 125 | HELPERS 126 | //////////////////////////////////////////////////////////////*/ 127 | 128 | /** 129 | * @notice ABI-encode a SignedKeyRequestMetadata struct. 130 | * 131 | * @param metadata The SignedKeyRequestMetadata struct to encode. 132 | * 133 | * @return bytes memory Bytes of ABI-encoded struct. 134 | */ 135 | function encodeMetadata(SignedKeyRequestMetadata calldata metadata) external pure returns (bytes memory) { 136 | return abi.encode(metadata); 137 | } 138 | 139 | /*////////////////////////////////////////////////////////////// 140 | ADMIN 141 | //////////////////////////////////////////////////////////////*/ 142 | 143 | /** 144 | * @notice Set the IdRegistry contract address. Only callable by owner. 145 | * 146 | * @param _idRegistry The new IdRegistry address. 147 | */ 148 | function setIdRegistry(address _idRegistry) external onlyOwner { 149 | emit SetIdRegistry(address(idRegistry), _idRegistry); 150 | idRegistry = IdRegistryLike(_idRegistry); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /test/IdRegistry/IdRegistry.owner.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import {IdRegistry} from "../../src/IdRegistry.sol"; 5 | import {IdRegistryTestSuite} from "./IdRegistryTestSuite.sol"; 6 | import {IGuardians} from "../../src/abstract/Guardians.sol"; 7 | 8 | /* solhint-disable state-visibility */ 9 | 10 | contract IdRegistryOwnerTest is IdRegistryTestSuite { 11 | /*////////////////////////////////////////////////////////////// 12 | EVENTS 13 | //////////////////////////////////////////////////////////////*/ 14 | 15 | event Add(address indexed guardian); 16 | event Remove(address indexed guardian); 17 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 18 | 19 | /*////////////////////////////////////////////////////////////// 20 | TRANSFER OWNERSHIP 21 | //////////////////////////////////////////////////////////////*/ 22 | 23 | function testFuzzTransferOwnership(address newOwner, address newOwner2) public { 24 | vm.assume(newOwner != address(0) && newOwner2 != address(0)); 25 | assertEq(idRegistry.owner(), owner); 26 | assertEq(idRegistry.pendingOwner(), address(0)); 27 | 28 | vm.prank(owner); 29 | idRegistry.transferOwnership(newOwner); 30 | assertEq(idRegistry.owner(), owner); 31 | assertEq(idRegistry.pendingOwner(), newOwner); 32 | 33 | vm.prank(owner); 34 | idRegistry.transferOwnership(newOwner2); 35 | assertEq(idRegistry.owner(), owner); 36 | assertEq(idRegistry.pendingOwner(), newOwner2); 37 | } 38 | 39 | function testFuzzCannotTransferOwnershipUnlessOwner(address alice, address newOwner) public { 40 | vm.assume(alice != owner && newOwner != address(0)); 41 | assertEq(idRegistry.owner(), owner); 42 | assertEq(idRegistry.pendingOwner(), address(0)); 43 | 44 | vm.prank(alice); 45 | vm.expectRevert("Ownable: caller is not the owner"); 46 | idRegistry.transferOwnership(newOwner); 47 | 48 | assertEq(idRegistry.owner(), owner); 49 | assertEq(idRegistry.pendingOwner(), address(0)); 50 | } 51 | 52 | /*////////////////////////////////////////////////////////////// 53 | ACCEPT OWNERSHIP 54 | //////////////////////////////////////////////////////////////*/ 55 | 56 | function testFuzzAcceptOwnership(address newOwner) public { 57 | vm.assume(newOwner != owner && newOwner != address(0)); 58 | vm.prank(owner); 59 | idRegistry.transferOwnership(newOwner); 60 | 61 | vm.expectEmit(); 62 | emit OwnershipTransferred(owner, newOwner); 63 | vm.prank(newOwner); 64 | idRegistry.acceptOwnership(); 65 | 66 | assertEq(idRegistry.owner(), newOwner); 67 | assertEq(idRegistry.pendingOwner(), address(0)); 68 | } 69 | 70 | function testFuzzCannotAcceptOwnershipUnlessPendingOwner(address alice, address newOwner) public { 71 | vm.assume(alice != owner && alice != address(0)); 72 | vm.assume(newOwner != alice && newOwner != address(0)); 73 | 74 | vm.prank(owner); 75 | idRegistry.transferOwnership(newOwner); 76 | 77 | vm.prank(alice); 78 | vm.expectRevert("Ownable2Step: caller is not the new owner"); 79 | idRegistry.acceptOwnership(); 80 | 81 | assertEq(idRegistry.owner(), owner); 82 | assertEq(idRegistry.pendingOwner(), newOwner); 83 | } 84 | 85 | /*////////////////////////////////////////////////////////////// 86 | PAUSE 87 | //////////////////////////////////////////////////////////////*/ 88 | 89 | function testPause() public { 90 | assertEq(idRegistry.owner(), owner); 91 | assertEq(idRegistry.paused(), false); 92 | 93 | _pause(); 94 | } 95 | 96 | function testAddRemoveGuardian(address guardian) public { 97 | assertEq(idRegistry.guardians(guardian), false); 98 | 99 | vm.expectEmit(); 100 | emit Add(guardian); 101 | 102 | vm.prank(owner); 103 | idRegistry.addGuardian(guardian); 104 | 105 | assertEq(idRegistry.guardians(guardian), true); 106 | 107 | vm.expectEmit(); 108 | emit Remove(guardian); 109 | 110 | vm.prank(owner); 111 | idRegistry.removeGuardian(guardian); 112 | 113 | assertEq(idRegistry.guardians(guardian), false); 114 | } 115 | 116 | function testFuzzCannotPauseUnlessGuardian(address alice) public { 117 | vm.assume(alice != owner && alice != address(0)); 118 | assertEq(idRegistry.owner(), owner); 119 | assertEq(idRegistry.paused(), false); 120 | 121 | vm.prank(alice); 122 | vm.expectRevert(IGuardians.OnlyGuardian.selector); 123 | idRegistry.pause(); 124 | 125 | assertEq(idRegistry.paused(), false); 126 | } 127 | 128 | function testUnpause() public { 129 | _pause(); 130 | 131 | vm.prank(owner); 132 | idRegistry.unpause(); 133 | 134 | assertEq(idRegistry.paused(), false); 135 | } 136 | 137 | function testFuzzCannotUnpauseUnlessOwner(address alice) public { 138 | vm.assume(alice != owner && alice != address(0)); 139 | assertEq(idRegistry.owner(), owner); 140 | _pause(); 141 | 142 | vm.prank(alice); 143 | vm.expectRevert("Ownable: caller is not the owner"); 144 | idRegistry.unpause(); 145 | 146 | assertEq(idRegistry.paused(), true); 147 | } 148 | 149 | function testCannotAddGuardianUnlessOwner(address caller, address guardian) public { 150 | vm.assume(caller != owner); 151 | assertEq(idRegistry.guardians(guardian), false); 152 | 153 | vm.prank(caller); 154 | vm.expectRevert("Ownable: caller is not the owner"); 155 | idRegistry.addGuardian(guardian); 156 | 157 | assertEq(idRegistry.guardians(guardian), false); 158 | } 159 | 160 | function testCannotRemoveGuardianUnlessOwner(address caller, address guardian) public { 161 | vm.assume(caller != owner); 162 | assertEq(idRegistry.guardians(guardian), false); 163 | 164 | vm.prank(caller); 165 | vm.expectRevert("Ownable: caller is not the owner"); 166 | idRegistry.addGuardian(guardian); 167 | 168 | assertEq(idRegistry.guardians(guardian), false); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/IdGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | import {IIdGateway} from "./interfaces/IIdGateway.sol"; 5 | import {IStorageRegistry} from "./interfaces/IStorageRegistry.sol"; 6 | import {IIdRegistry} from "./interfaces/IIdRegistry.sol"; 7 | import {Guardians} from "./abstract/Guardians.sol"; 8 | import {TransferHelper} from "./libraries/TransferHelper.sol"; 9 | import {EIP712} from "./abstract/EIP712.sol"; 10 | import {Nonces} from "./abstract/Nonces.sol"; 11 | import {Signatures} from "./abstract/Signatures.sol"; 12 | 13 | /** 14 | * @title Farcaster IdGateway 15 | * 16 | * @notice See https://github.com/farcasterxyz/contracts/blob/v3.1.0/docs/docs.md for an overview. 17 | * 18 | * @custom:security-contact security@merklemanufactory.com 19 | */ 20 | contract IdGateway is IIdGateway, Guardians, Signatures, EIP712, Nonces { 21 | using TransferHelper for address; 22 | 23 | /*////////////////////////////////////////////////////////////// 24 | CONSTANTS 25 | //////////////////////////////////////////////////////////////*/ 26 | 27 | /** 28 | * @inheritdoc IIdGateway 29 | */ 30 | string public constant VERSION = "2023.11.15"; 31 | 32 | /** 33 | * @inheritdoc IIdGateway 34 | */ 35 | bytes32 public constant REGISTER_TYPEHASH = 36 | keccak256("Register(address to,address recovery,uint256 nonce,uint256 deadline)"); 37 | 38 | /*////////////////////////////////////////////////////////////// 39 | IMMUTABLES 40 | //////////////////////////////////////////////////////////////*/ 41 | 42 | /** 43 | * @inheritdoc IIdGateway 44 | */ 45 | IIdRegistry public immutable idRegistry; 46 | 47 | /*////////////////////////////////////////////////////////////// 48 | STORAGE 49 | //////////////////////////////////////////////////////////////*/ 50 | 51 | /** 52 | * @inheritdoc IIdGateway 53 | */ 54 | IStorageRegistry public storageRegistry; 55 | 56 | /*////////////////////////////////////////////////////////////// 57 | CONSTRUCTOR 58 | //////////////////////////////////////////////////////////////*/ 59 | 60 | /** 61 | * @notice Configure IdRegistry and StorageRegistry addresses. 62 | * Set the owner of the contract to the provided _owner. 63 | * 64 | * @param _idRegistry IdRegistry address. 65 | * @param _storageRegistry StorageRegistery address. 66 | * @param _initialOwner Initial owner address. 67 | * 68 | */ 69 | constructor( 70 | address _idRegistry, 71 | address _storageRegistry, 72 | address _initialOwner 73 | ) Guardians(_initialOwner) EIP712("Farcaster IdGateway", "1") { 74 | idRegistry = IIdRegistry(_idRegistry); 75 | storageRegistry = IStorageRegistry(_storageRegistry); 76 | emit SetStorageRegistry(address(0), _storageRegistry); 77 | } 78 | 79 | /*////////////////////////////////////////////////////////////// 80 | PRICE VIEW 81 | //////////////////////////////////////////////////////////////*/ 82 | 83 | /** 84 | * @inheritdoc IIdGateway 85 | */ 86 | function price() external view returns (uint256) { 87 | return storageRegistry.unitPrice(); 88 | } 89 | 90 | /** 91 | * @inheritdoc IIdGateway 92 | */ 93 | function price(uint256 extraStorage) external view returns (uint256) { 94 | return storageRegistry.price(1 + extraStorage); 95 | } 96 | 97 | /*////////////////////////////////////////////////////////////// 98 | REGISTRATION LOGIC 99 | //////////////////////////////////////////////////////////////*/ 100 | 101 | /** 102 | * @inheritdoc IIdGateway 103 | */ 104 | function register(address recovery) external payable returns (uint256, uint256) { 105 | return register(recovery, 0); 106 | } 107 | 108 | function register( 109 | address recovery, 110 | uint256 extraStorage 111 | ) public payable whenNotPaused returns (uint256 fid, uint256 overpayment) { 112 | fid = idRegistry.register(msg.sender, recovery); 113 | overpayment = _rentStorage(fid, extraStorage, msg.value, msg.sender); 114 | } 115 | 116 | /** 117 | * @inheritdoc IIdGateway 118 | */ 119 | function registerFor( 120 | address to, 121 | address recovery, 122 | uint256 deadline, 123 | bytes calldata sig 124 | ) external payable returns (uint256, uint256) { 125 | return registerFor(to, recovery, deadline, sig, 0); 126 | } 127 | 128 | function registerFor( 129 | address to, 130 | address recovery, 131 | uint256 deadline, 132 | bytes calldata sig, 133 | uint256 extraStorage 134 | ) public payable whenNotPaused returns (uint256 fid, uint256 overpayment) { 135 | /* Revert if signature is invalid */ 136 | _verifyRegisterSig({to: to, recovery: recovery, deadline: deadline, sig: sig}); 137 | fid = idRegistry.register(to, recovery); 138 | overpayment = _rentStorage(fid, extraStorage, msg.value, msg.sender); 139 | } 140 | 141 | /*////////////////////////////////////////////////////////////// 142 | PERMISSIONED ACTIONS 143 | //////////////////////////////////////////////////////////////*/ 144 | 145 | /** 146 | * @inheritdoc IIdGateway 147 | */ 148 | function setStorageRegistry(address _storageRegistry) external onlyOwner { 149 | emit SetStorageRegistry(address(storageRegistry), _storageRegistry); 150 | storageRegistry = IStorageRegistry(_storageRegistry); 151 | } 152 | 153 | /*////////////////////////////////////////////////////////////// 154 | SIGNATURE VERIFICATION HELPERS 155 | //////////////////////////////////////////////////////////////*/ 156 | 157 | function _verifyRegisterSig(address to, address recovery, uint256 deadline, bytes memory sig) internal { 158 | _verifySig( 159 | _hashTypedDataV4(keccak256(abi.encode(REGISTER_TYPEHASH, to, recovery, _useNonce(to), deadline))), 160 | to, 161 | deadline, 162 | sig 163 | ); 164 | } 165 | 166 | /*////////////////////////////////////////////////////////////// 167 | STORAGE RENTAL HELPERS 168 | //////////////////////////////////////////////////////////////*/ 169 | 170 | function _rentStorage( 171 | uint256 fid, 172 | uint256 extraUnits, 173 | uint256 payment, 174 | address payer 175 | ) internal returns (uint256 overpayment) { 176 | overpayment = storageRegistry.rent{value: payment}(fid, 1 + extraUnits); 177 | 178 | if (overpayment > 0) { 179 | payer.sendNative(overpayment); 180 | } 181 | } 182 | 183 | receive() external payable { 184 | if (msg.sender != address(storageRegistry)) revert Unauthorized(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /test/KeyRegistry/KeyRegistry.integration.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import "forge-std/console.sol"; 5 | 6 | import {KeyRegistry, IKeyRegistry} from "../../src/KeyRegistry.sol"; 7 | import {IMetadataValidator} from "../../src/interfaces/IMetadataValidator.sol"; 8 | import {SignedKeyRequestValidator} from "../../src/validators/SignedKeyRequestValidator.sol"; 9 | 10 | import {SignedKeyRequestValidatorTestSuite} from 11 | "../validators/SignedKeyRequestValidator/SignedKeyRequestValidatorTestSuite.sol"; 12 | import {KeyRegistryTestSuite} from "./KeyRegistryTestSuite.sol"; 13 | 14 | /* solhint-disable state-visibility */ 15 | 16 | contract KeyRegistryIntegrationTest is KeyRegistryTestSuite, SignedKeyRequestValidatorTestSuite { 17 | function setUp() public override(KeyRegistryTestSuite, SignedKeyRequestValidatorTestSuite) { 18 | super.setUp(); 19 | 20 | vm.prank(owner); 21 | keyRegistry.setValidator(1, 1, IMetadataValidator(address(validator))); 22 | } 23 | 24 | event Add( 25 | uint256 indexed fid, 26 | uint32 indexed keyType, 27 | bytes indexed key, 28 | bytes keyBytes, 29 | uint8 metadataType, 30 | bytes metadata 31 | ); 32 | 33 | function testFuzzAdd( 34 | address to, 35 | uint256 signerPk, 36 | address recovery, 37 | bytes calldata _keyBytes, 38 | uint40 _deadline 39 | ) public { 40 | signerPk = _boundPk(signerPk); 41 | uint256 deadline = _boundDeadline(_deadline); 42 | address signer = vm.addr(signerPk); 43 | vm.assume(signer != to); 44 | 45 | uint256 userFid = _registerFid(to, recovery); 46 | uint256 requestFid = _register(signer); 47 | bytes memory key = _validKey(_keyBytes); 48 | 49 | bytes memory sig = _signMetadata(signerPk, requestFid, key, deadline); 50 | 51 | bytes memory metadata = abi.encode( 52 | SignedKeyRequestValidator.SignedKeyRequestMetadata({ 53 | requestFid: requestFid, 54 | requestSigner: signer, 55 | signature: sig, 56 | deadline: deadline 57 | }) 58 | ); 59 | 60 | vm.expectEmit(); 61 | emit Add(userFid, 1, key, key, 1, metadata); 62 | vm.prank(keyRegistry.keyGateway()); 63 | keyRegistry.add(to, 1, key, 1, metadata); 64 | 65 | assertAdded(userFid, key, 1); 66 | } 67 | 68 | function testFuzzAddRevertsShortKey( 69 | address to, 70 | uint256 signerPk, 71 | address recovery, 72 | bytes calldata _keyBytes, 73 | uint40 _deadline, 74 | uint8 _shortenBy 75 | ) public { 76 | _registerFid(to, recovery); 77 | bytes memory key = _shortKey(_keyBytes, _shortenBy); 78 | 79 | signerPk = _boundPk(signerPk); 80 | uint256 deadline = _boundDeadline(_deadline); 81 | address signer = vm.addr(signerPk); 82 | vm.assume(signer != to); 83 | 84 | uint256 requestFid = _register(signer); 85 | 86 | bytes memory sig = _signMetadata(signerPk, requestFid, key, deadline); 87 | 88 | bytes memory metadata = abi.encode( 89 | SignedKeyRequestValidator.SignedKeyRequestMetadata({ 90 | requestFid: requestFid, 91 | requestSigner: signer, 92 | signature: sig, 93 | deadline: deadline 94 | }) 95 | ); 96 | 97 | vm.prank(keyRegistry.keyGateway()); 98 | vm.expectRevert(IKeyRegistry.InvalidMetadata.selector); 99 | keyRegistry.add(to, 1, key, 1, metadata); 100 | } 101 | 102 | function testFuzzAddRevertsLongKey( 103 | address to, 104 | uint256 signerPk, 105 | address recovery, 106 | bytes calldata _keyBytes, 107 | uint40 _deadline, 108 | uint8 _lengthenBy 109 | ) public { 110 | _registerFid(to, recovery); 111 | bytes memory key = _longKey(_keyBytes, _lengthenBy); 112 | 113 | signerPk = _boundPk(signerPk); 114 | uint256 deadline = _boundDeadline(_deadline); 115 | address signer = vm.addr(signerPk); 116 | vm.assume(signer != to); 117 | 118 | uint256 requestFid = _register(signer); 119 | 120 | bytes memory sig = _signMetadata(signerPk, requestFid, key, deadline); 121 | 122 | bytes memory metadata = abi.encode( 123 | SignedKeyRequestValidator.SignedKeyRequestMetadata({ 124 | requestFid: requestFid, 125 | requestSigner: signer, 126 | signature: sig, 127 | deadline: deadline 128 | }) 129 | ); 130 | 131 | vm.prank(keyRegistry.keyGateway()); 132 | vm.expectRevert(IKeyRegistry.InvalidMetadata.selector); 133 | keyRegistry.add(to, 1, key, 1, metadata); 134 | } 135 | 136 | function testFuzzAddRevertsInvalidSig( 137 | address to, 138 | uint256 signerPk, 139 | uint256 otherPk, 140 | address recovery, 141 | bytes calldata key, 142 | uint40 _deadline 143 | ) public { 144 | signerPk = _boundPk(signerPk); 145 | otherPk = _boundPk(otherPk); 146 | uint256 deadline = _boundDeadline(_deadline); 147 | vm.assume(signerPk != otherPk); 148 | address signer = vm.addr(signerPk); 149 | vm.assume(signer != to); 150 | 151 | _registerFid(to, recovery); 152 | uint256 requestFid = _register(signer); 153 | 154 | bytes memory sig = _signMetadata(otherPk, requestFid, key, deadline); 155 | 156 | bytes memory metadata = abi.encode( 157 | SignedKeyRequestValidator.SignedKeyRequestMetadata({ 158 | requestFid: requestFid, 159 | requestSigner: signer, 160 | signature: sig, 161 | deadline: deadline 162 | }) 163 | ); 164 | 165 | vm.prank(keyRegistry.keyGateway()); 166 | vm.expectRevert(IKeyRegistry.InvalidMetadata.selector); 167 | keyRegistry.add(to, 1, key, 1, metadata); 168 | } 169 | 170 | /*////////////////////////////////////////////////////////////// 171 | HELPERS 172 | //////////////////////////////////////////////////////////////*/ 173 | 174 | function _registerFid(address to, address recovery) internal returns (uint256) { 175 | vm.prank(idRegistry.idGateway()); 176 | return idRegistry.register(to, recovery); 177 | } 178 | 179 | function assertEq(IKeyRegistry.KeyState a, IKeyRegistry.KeyState b) internal { 180 | assertEq(uint8(a), uint8(b)); 181 | } 182 | 183 | function assertNull(uint256 fid, bytes memory key) internal { 184 | assertEq(keyRegistry.keyDataOf(fid, key).state, IKeyRegistry.KeyState.NULL); 185 | assertEq(keyRegistry.keyDataOf(fid, key).keyType, 0); 186 | } 187 | 188 | function assertAdded(uint256 fid, bytes memory key, uint32 keyType) internal { 189 | assertEq(keyRegistry.keyDataOf(fid, key).state, IKeyRegistry.KeyState.ADDED); 190 | assertEq(keyRegistry.keyDataOf(fid, key).keyType, keyType); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | # CI is run on main because new branches can only access caches from master, not previous branches. 4 | # So building on master allows new PR's to get the cache from before. 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | env: 11 | FOUNDRY_PROFILE: ci 12 | L1_MAINNET_RPC_URL: ${{ secrets.L1_MAINNET_RPC_URL }} 13 | L2_MAINNET_RPC_URL: ${{ secrets.L2_MAINNET_RPC_URL }} 14 | 15 | jobs: 16 | build-image: 17 | timeout-minutes: 5 18 | runs-on: ${{ vars.BUILDJET_DISABLED == 'true' && 'ubuntu-latest' || 'buildjet-16vcpu-ubuntu-2204' }} 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | submodules: recursive 23 | 24 | - name: Install foundry 25 | uses: foundry-rs/foundry-toolchain@v1 26 | with: 27 | version: nightly 28 | 29 | - name: Install Docker buildx 30 | uses: docker/setup-buildx-action@v2 31 | 32 | - name: Copy .env.local 33 | run: cp .env.local .env 34 | shell: bash 35 | 36 | - name: Build Docker images defined in Docker Compose file 37 | uses: docker/bake-action@v3 38 | with: 39 | load: true # Load images into local Docker engine after build 40 | 41 | - name: Run containers defined in Docker Compose 42 | shell: bash 43 | run: docker compose up --detach 44 | 45 | - name: Check that Anvil is running 46 | uses: nick-fields/retry@v2 47 | with: 48 | timeout_seconds: 15 49 | retry_wait_seconds: 5 50 | max_attempts: 10 51 | shell: bash 52 | command: '[ "$(cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266)" = 4096000000000000000000 ]' # Default address 53 | on_retry_command: docker compose logs && docker compose ps && cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 54 | 55 | - name: Wait for contract to be deployed 56 | uses: nick-fields/retry@v2 57 | with: 58 | timeout_seconds: 5 59 | retry_wait_seconds: 5 60 | max_attempts: 10 61 | shell: bash 62 | command: | 63 | set -e -o pipefail 64 | docker compose logs | grep Bundler | awk '{ print $5 }' 65 | on_retry_command: docker compose logs 66 | 67 | - name: Get contract addresses 68 | run: | 69 | echo "ID_CONTRACT_ADDRESS=$(docker compose logs | grep IdRegistry | awk '{ print $5 }')" >> $GITHUB_ENV 70 | echo "KEY_CONTRACT_ADDRESS=$(docker compose logs | grep KeyRegistry | awk '{ print $5 }')" >> $GITHUB_ENV 71 | echo "STORAGE_CONTRACT_ADDRESS=$(docker compose logs | grep StorageRegistry | awk '{ print $5 }')" >> $GITHUB_ENV 72 | echo "BUNDLER_CONTRACT_ADDRESS=$(docker compose logs | grep Bundler | awk '{ print $5 }')" >> $GITHUB_ENV 73 | shell: bash 74 | 75 | - name: Confirm ID Registry contract was deployed 76 | shell: bash 77 | run: '[ $(cast call $ID_CONTRACT_ADDRESS "owner()") = 0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266 ]' 78 | 79 | - name: Confirm Key Registry contract was deployed 80 | shell: bash 81 | run: '[ $(cast call $KEY_CONTRACT_ADDRESS "owner()") = 0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266 ]' 82 | 83 | - name: Confirm Storage Registry contract was deployed 84 | shell: bash 85 | run: '[ $(cast call $STORAGE_CONTRACT_ADDRESS "paused()") = 0x0000000000000000000000000000000000000000000000000000000000000000 ]' 86 | 87 | - name: Confirm Bundler contract was deployed 88 | shell: bash 89 | run: '[ $(cast call $BUNDLER_CONTRACT_ADDRESS "VERSION()") = 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a323032332e31312e313500000000000000000000000000000000000000000000 ]' 90 | 91 | test: 92 | strategy: 93 | fail-fast: true 94 | 95 | timeout-minutes: 15 96 | runs-on: ${{ vars.BUILDJET_DISABLED == 'true' && 'ubuntu-latest' || 'buildjet-16vcpu-ubuntu-2204' }} 97 | steps: 98 | - uses: actions/checkout@v3 99 | with: 100 | submodules: recursive 101 | 102 | - name: Install foundry 103 | uses: foundry-rs/foundry-toolchain@v1 104 | with: 105 | version: nightly 106 | 107 | - name: Run forge build 108 | run: | 109 | forge --version 110 | forge build --sizes 111 | 112 | - name: Run forge fmt 113 | run: forge fmt --check 114 | 115 | - name: Run forge tests 116 | run: forge test -vvv 117 | 118 | - name: Check forge snapshots 119 | run: forge snapshot --check --match-contract Gas 120 | 121 | halmos: 122 | runs-on: ${{ vars.BUILDJET_DISABLED == 'true' && 'macos-latest' || 'buildjet-16vcpu-ubuntu-2204' }} 123 | steps: 124 | - uses: actions/checkout@v3 125 | with: 126 | submodules: recursive 127 | 128 | - name: Install foundry 129 | uses: foundry-rs/foundry-toolchain@v1 130 | 131 | - uses: actions/setup-python@v4 132 | with: 133 | python-version: "3.11" 134 | 135 | - name: Install halmos 136 | run: pip install halmos 137 | 138 | - name: Run halmos 139 | run: halmos --error-unknown --test-parallel --solver-parallel --storage-layout=generic --solver-timeout-assertion 0 140 | 141 | coverage: 142 | permissions: 143 | contents: read 144 | pull-requests: write 145 | runs-on: ${{ vars.BUILDJET_DISABLED == 'true' && 'ubuntu-latest' || 'buildjet-2vcpu-ubuntu-2204' }} 146 | steps: 147 | - uses: actions/checkout@v3 148 | 149 | - name: Install Foundry 150 | uses: foundry-rs/foundry-toolchain@v1 151 | 152 | - name: Check code coverage 153 | run: forge coverage --report summary --report lcov 154 | 155 | # Ignores coverage results for the test and script directories. Note that because this 156 | # filtering applies to the lcov file, the summary table generated in the previous step will 157 | # still include all files and directories. 158 | # The `--rc lcov_branch_coverage=1` part keeps branch info in the filtered report, since lcov 159 | # defaults to removing branch info. 160 | - name: Filter directories 161 | run: | 162 | sudo apt update && sudo apt install -y lcov 163 | lcov --remove lcov.info 'test/*' 'script/*' 'src/libraries/*' --output-file lcov.info --rc lcov_branch_coverage=1 164 | 165 | # Post a detailed coverage report as a comment and deletes previous comments on each push. 166 | - name: Post coverage report 167 | if: github.event_name == 'pull_request' # This action fails when ran outside of a pull request. 168 | uses: romeovs/lcov-reporter-action@v0.3.1 169 | with: 170 | delete-old-comments: true 171 | lcov-file: ./lcov.info 172 | github-token: ${{ secrets.GITHUB_TOKEN }} # Adds a coverage summary comment to the PR. 173 | 174 | # Fail coverage if the specified coverage threshold is not met 175 | - name: Verify minimum coverage 176 | uses: zgosalvez/github-actions-report-lcov@v2 177 | with: 178 | coverage-files: ./lcov.info 179 | minimum-coverage: 94 180 | -------------------------------------------------------------------------------- /script/abstract/ImmutableCreate2Deployer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | import "forge-std/Script.sol"; 5 | import "forge-std/console.sol"; 6 | import {Strings} from "openzeppelin/contracts/utils/Strings.sol"; 7 | 8 | interface ImmutableCreate2Factory { 9 | function hasBeenDeployed(address deploymentAddress) external view returns (bool); 10 | 11 | function findCreate2Address( 12 | bytes32 salt, 13 | bytes calldata initializationCode 14 | ) external view returns (address deploymentAddress); 15 | 16 | function safeCreate2( 17 | bytes32 salt, 18 | bytes calldata initializationCode 19 | ) external payable returns (address deploymentAddress); 20 | } 21 | 22 | abstract contract ImmutableCreate2Deployer is Script { 23 | enum Status { 24 | UNKNOWN, 25 | FOUND, 26 | DEPLOYED 27 | } 28 | 29 | /** 30 | * @dev Deployment information for a contract. 31 | * 32 | * @param name Contract name 33 | * @param salt CREATE2 salt 34 | * @param creationCode Contract creationCode bytes 35 | * @param constructorArgs ABI-encoded constructor argument bytes 36 | * @param initCodeHash Contract initCode (creationCode + constructorArgs) hash 37 | * @param deploymentAddress Deterministic deployment address 38 | */ 39 | struct Deployment { 40 | string name; 41 | bytes32 salt; 42 | bytes creationCode; 43 | bytes constructorArgs; 44 | bytes32 initCodeHash; 45 | address deploymentAddress; 46 | Status status; 47 | } 48 | 49 | /// @dev Deterministic address of the cross-chain ImmutableCreate2Factory 50 | ImmutableCreate2Factory private constant IMMUTABLE_CREATE2_FACTORY = 51 | ImmutableCreate2Factory(0x0000000000FFe8B47B3e2130213B802212439497); 52 | 53 | /// @dev Default CREATE2 salt 54 | bytes32 private constant DEFAULT_SALT = bytes32(0); 55 | 56 | /// @dev Array of contract names, used to track contracts "registered" for later deployment. 57 | string[] internal names; 58 | 59 | /// @dev Mapping of contract name to deployment details. 60 | mapping(string name => Deployment deployment) internal contracts; 61 | 62 | /** 63 | * @dev "Register" a contract to be deployed by deploy(). 64 | * 65 | * @param name Contract name 66 | * @param creationCode Contract creationCode bytes 67 | */ 68 | function register(string memory name, bytes memory creationCode) internal returns (address) { 69 | return register(name, DEFAULT_SALT, creationCode, ""); 70 | } 71 | 72 | /** 73 | * @dev "Register" a contract to be deployed by deploy(). 74 | * 75 | * @param name Contract name 76 | * @param salt CREATE2 salt 77 | * @param creationCode Contract creationCode bytes 78 | */ 79 | function register(string memory name, bytes32 salt, bytes memory creationCode) internal returns (address) { 80 | return register(name, salt, creationCode, ""); 81 | } 82 | 83 | /** 84 | * @dev "Register" a contract to be deployed by deploy(). 85 | * 86 | * @param name Contract name 87 | * @param creationCode Contract creationCode bytes 88 | * @param constructorArgs ABI-encoded constructor argument bytes 89 | */ 90 | function register( 91 | string memory name, 92 | bytes memory creationCode, 93 | bytes memory constructorArgs 94 | ) internal returns (address) { 95 | return register(name, DEFAULT_SALT, creationCode, constructorArgs); 96 | } 97 | 98 | /** 99 | * @dev "Register" a contract to be deployed by deploy(). 100 | * 101 | * @param name Contract name 102 | * @param salt CREATE2 salt 103 | * @param creationCode Contract creationCode bytes 104 | * @param constructorArgs ABI-encoded constructor argument bytes 105 | */ 106 | function register( 107 | string memory name, 108 | bytes32 salt, 109 | bytes memory creationCode, 110 | bytes memory constructorArgs 111 | ) internal returns (address) { 112 | bytes memory initCode = bytes.concat(creationCode, constructorArgs); 113 | bytes32 initCodeHash = keccak256(initCode); 114 | address deploymentAddress = address( 115 | uint160( 116 | uint256(keccak256(abi.encodePacked(hex"ff", address(IMMUTABLE_CREATE2_FACTORY), salt, initCodeHash))) 117 | ) 118 | ); 119 | names.push(name); 120 | contracts[name] = Deployment({ 121 | name: name, 122 | salt: salt, 123 | creationCode: creationCode, 124 | constructorArgs: constructorArgs, 125 | initCodeHash: initCodeHash, 126 | deploymentAddress: deploymentAddress, 127 | status: Status.UNKNOWN 128 | }); 129 | return deploymentAddress; 130 | } 131 | 132 | /** 133 | * @dev Deploy all registered contracts. 134 | */ 135 | function deploy(bool broadcast) internal { 136 | console.log(pad("State", 10), pad("Name", 27), pad("Address", 43), "Initcode hash"); 137 | for (uint256 i; i < names.length; i++) { 138 | _deploy(names[i], broadcast); 139 | } 140 | } 141 | 142 | function deploy() internal { 143 | deploy(true); 144 | } 145 | 146 | /** 147 | * @dev Deploy a registered contract by name. 148 | * 149 | * @param name Contract name 150 | */ 151 | function deploy(string memory name, bool broadcast) public { 152 | console.log(pad("State", 10), pad("Name", 17), pad("Address", 43), "Initcode hash"); 153 | _deploy(name, broadcast); 154 | } 155 | 156 | function deploy(string memory name) internal { 157 | deploy(name, true); 158 | } 159 | 160 | function _deploy(string memory name, bool broadcast) internal { 161 | Deployment storage deployment = contracts[name]; 162 | if (!IMMUTABLE_CREATE2_FACTORY.hasBeenDeployed(deployment.deploymentAddress)) { 163 | if (broadcast) vm.broadcast(); 164 | deployment.deploymentAddress = IMMUTABLE_CREATE2_FACTORY.safeCreate2( 165 | deployment.salt, bytes.concat(deployment.creationCode, deployment.constructorArgs) 166 | ); 167 | deployment.status = Status.DEPLOYED; 168 | } else { 169 | deployment.status = Status.FOUND; 170 | } 171 | console.log( 172 | pad((deployment.status == Status.DEPLOYED) ? "Deploying" : "Found", 10), 173 | pad(deployment.name, 27), 174 | pad(Strings.toHexString(deployment.deploymentAddress), 43), 175 | Strings.toHexString(uint256(deployment.initCodeHash)) 176 | ); 177 | } 178 | 179 | function deploymentChanged() public view returns (bool) { 180 | for (uint256 i; i < names.length; i++) { 181 | Deployment storage deployment = contracts[names[i]]; 182 | if (deployment.status == Status.DEPLOYED) { 183 | return true; 184 | } 185 | } 186 | return false; 187 | } 188 | 189 | /** 190 | * @dev Pad string to given length. 191 | * 192 | * @param str string to pad 193 | * @param n length to pad to 194 | */ 195 | function pad(string memory str, uint256 n) internal pure returns (string memory) { 196 | string memory padded = str; 197 | while (bytes(padded).length < n) { 198 | padded = string.concat(padded, " "); 199 | } 200 | return padded; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /test/FnameResolver/FnameResolver.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.19; 3 | 4 | import "forge-std/Test.sol"; 5 | import {IERC165} from "openzeppelin/contracts/utils/introspection/IERC165.sol"; 6 | 7 | import {FnameResolverTestSuite} from "./FnameResolverTestSuite.sol"; 8 | import {FnameResolver, IResolverService, IExtendedResolver, IAddressQuery} from "../../src/FnameResolver.sol"; 9 | 10 | /* solhint-disable state-visibility */ 11 | 12 | contract FnameResolverTest is FnameResolverTestSuite { 13 | event AddSigner(address indexed signer); 14 | event RemoveSigner(address indexed signer); 15 | 16 | function testURL() public { 17 | assertEq(resolver.url(), FNAME_SERVER_URL); 18 | } 19 | 20 | function testInitialOwner() public { 21 | assertEq(resolver.owner(), owner); 22 | } 23 | 24 | function testSignerIsAuthorized() public { 25 | assertEq(resolver.signers(signer), true); 26 | } 27 | 28 | function testVersion() public { 29 | assertEq(resolver.VERSION(), "2023.08.23"); 30 | } 31 | 32 | /*////////////////////////////////////////////////////////////// 33 | RESOLVE 34 | //////////////////////////////////////////////////////////////*/ 35 | 36 | function testFuzzResolveRevertsWithOffchainLookup(bytes calldata name, bytes memory data) public { 37 | data = bytes.concat(IAddressQuery.addr.selector, data); 38 | string[] memory urls = new string[](1); 39 | urls[0] = FNAME_SERVER_URL; 40 | 41 | bytes memory callData = abi.encodeCall(resolver.resolve, (name, data)); 42 | bytes memory offchainLookup = abi.encodeWithSelector( 43 | FnameResolver.OffchainLookup.selector, 44 | address(resolver), 45 | urls, 46 | callData, 47 | resolver.resolveWithProof.selector, 48 | callData 49 | ); 50 | vm.expectRevert(offchainLookup); 51 | resolver.resolve(name, data); 52 | } 53 | 54 | function testFuzzResolveRevertsNonAddrFunction(bytes calldata name, bytes memory data) public { 55 | data = bytes.concat(hex"00000001", data); 56 | string[] memory urls = new string[](1); 57 | urls[0] = FNAME_SERVER_URL; 58 | 59 | vm.expectRevert(FnameResolver.ResolverFunctionNotSupported.selector); 60 | resolver.resolve(name, data); 61 | } 62 | 63 | /*////////////////////////////////////////////////////////////// 64 | RESOLVE WITH PROOF 65 | //////////////////////////////////////////////////////////////*/ 66 | 67 | function testFuzzResolveWithProofValidSignature(string memory name, uint256 timestamp, address owner) public { 68 | bytes memory signature = _signProof(name, timestamp, owner); 69 | bytes memory extraData = abi.encodeCall(IResolverService.resolve, (DNS_ENCODED_NAME, ADDR_QUERY_CALLDATA)); 70 | bytes memory response = resolver.resolveWithProof(abi.encode(name, timestamp, owner, signature), extraData); 71 | assertEq(response, abi.encode(owner)); 72 | } 73 | 74 | function testFuzzResolveWithProofInvalidOwner(string memory name, uint256 timestamp, address owner) public { 75 | address wrongOwner = address(~uint160(owner)); 76 | bytes memory signature = _signProof(name, timestamp, owner); 77 | 78 | vm.expectRevert(FnameResolver.InvalidSigner.selector); 79 | resolver.resolveWithProof(abi.encode(name, timestamp, wrongOwner, signature), ""); 80 | } 81 | 82 | function testFuzzResolveWithProofInvalidTimestamp(string memory name, uint256 timestamp, address owner) public { 83 | uint256 wrongTimestamp = ~timestamp; 84 | bytes memory signature = _signProof(name, timestamp, owner); 85 | 86 | vm.expectRevert(FnameResolver.InvalidSigner.selector); 87 | resolver.resolveWithProof(abi.encode(name, wrongTimestamp, owner, signature), ""); 88 | } 89 | 90 | function testFuzzResolveWithProofInvalidName(string memory name, uint256 timestamp, address owner) public { 91 | string memory wrongName = string.concat("~", name); 92 | bytes memory signature = _signProof(name, timestamp, owner); 93 | 94 | vm.expectRevert(FnameResolver.InvalidSigner.selector); 95 | resolver.resolveWithProof(abi.encode(wrongName, timestamp, owner, signature), ""); 96 | } 97 | 98 | function testFuzzResolveWithProofWrongSigner(string memory name, uint256 timestamp, address owner) public { 99 | bytes memory signature = _signProof(malloryPk, name, timestamp, owner); 100 | 101 | vm.expectRevert(FnameResolver.InvalidSigner.selector); 102 | resolver.resolveWithProof(abi.encode(name, timestamp, owner, signature), ""); 103 | } 104 | 105 | function testFuzzResolveWithProofInvalidSignerLength( 106 | string memory name, 107 | uint256 timestamp, 108 | address owner, 109 | bytes memory signature, 110 | uint8 _length 111 | ) public { 112 | vm.assume(signature.length >= 65); 113 | uint256 length = bound(_length, 0, 64); 114 | assembly { 115 | mstore(signature, length) 116 | } /* truncate signature length */ 117 | 118 | vm.expectRevert("ECDSA: invalid signature length"); 119 | resolver.resolveWithProof(abi.encode(name, timestamp, owner, signature), ""); 120 | } 121 | 122 | function testProofTypehash() public { 123 | assertEq( 124 | resolver.USERNAME_PROOF_TYPEHASH(), keccak256("UserNameProof(string name,uint256 timestamp,address owner)") 125 | ); 126 | } 127 | 128 | /*////////////////////////////////////////////////////////////// 129 | SIGNERS 130 | //////////////////////////////////////////////////////////////*/ 131 | 132 | function testFuzzOwnerCanAddSigner(address signer) public { 133 | vm.expectEmit(true, false, false, false); 134 | emit AddSigner(signer); 135 | 136 | vm.prank(owner); 137 | resolver.addSigner(signer); 138 | 139 | assertEq(resolver.signers(signer), true); 140 | } 141 | 142 | function testFuzzOnlyOwnerCanAddSigner(address caller, address signer) public { 143 | vm.assume(caller != owner); 144 | 145 | vm.prank(caller); 146 | vm.expectRevert("Ownable: caller is not the owner"); 147 | resolver.addSigner(signer); 148 | } 149 | 150 | function testFuzzOwnerCanRemoveSigner(address signer) public { 151 | vm.prank(owner); 152 | resolver.addSigner(signer); 153 | 154 | assertEq(resolver.signers(signer), true); 155 | 156 | vm.expectEmit(true, false, false, false); 157 | emit RemoveSigner(signer); 158 | 159 | vm.prank(owner); 160 | resolver.removeSigner(signer); 161 | 162 | assertEq(resolver.signers(signer), false); 163 | } 164 | 165 | function testFuzzOnlyOwnerCanRemoveSigner(address caller, address signer) public { 166 | vm.assume(caller != owner); 167 | 168 | vm.prank(caller); 169 | vm.expectRevert("Ownable: caller is not the owner"); 170 | resolver.removeSigner(signer); 171 | } 172 | 173 | /*////////////////////////////////////////////////////////////// 174 | INTERFACE DETECTION 175 | //////////////////////////////////////////////////////////////*/ 176 | 177 | function testInterfaceDetectionIExtendedResolver() public { 178 | assertEq(resolver.supportsInterface(type(IExtendedResolver).interfaceId), true); 179 | } 180 | 181 | function testInterfaceDetectionERC165() public { 182 | assertEq(resolver.supportsInterface(type(IERC165).interfaceId), true); 183 | } 184 | 185 | function testFuzzInterfaceDetectionUnsupportedInterface(bytes4 interfaceId) public { 186 | vm.assume(interfaceId != type(IExtendedResolver).interfaceId && interfaceId != type(IERC165).interfaceId); 187 | assertEq(resolver.supportsInterface(interfaceId), false); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/FnameResolver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.21; 3 | 4 | import {Ownable2Step} from "openzeppelin/contracts/access/Ownable2Step.sol"; 5 | import {ECDSA} from "openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 6 | import {ERC165} from "openzeppelin/contracts/utils/introspection/ERC165.sol"; 7 | 8 | import {EIP712} from "./abstract/EIP712.sol"; 9 | 10 | interface IAddressQuery { 11 | function addr(bytes32 node) external view returns (address); 12 | } 13 | 14 | interface IExtendedResolver { 15 | function resolve(bytes memory name, bytes memory data) external view returns (bytes memory); 16 | } 17 | 18 | interface IResolverService { 19 | function resolve( 20 | bytes calldata name, 21 | bytes calldata data 22 | ) external view returns (string memory fname, uint256 timestamp, address owner, bytes memory signature); 23 | } 24 | 25 | /** 26 | * @title Farcaster FnameResolver 27 | * 28 | * @notice See https://github.com/farcasterxyz/contracts/blob/v3.1.0/docs/docs.md for an overview. 29 | * 30 | * @custom:security-contact security@merklemanufactory.com 31 | */ 32 | contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step { 33 | /*////////////////////////////////////////////////////////////// 34 | ERRORS 35 | //////////////////////////////////////////////////////////////*/ 36 | 37 | /** 38 | * @dev Revert to indicate an offchain CCIP lookup. See: https://eips.ethereum.org/EIPS/eip-3668 39 | * 40 | * @param sender Address of this contract. 41 | * @param urls List of lookup gateway URLs. 42 | * @param callData Data to call the gateway with. 43 | * @param callbackFunction 4 byte function selector of the callback function on this contract. 44 | * @param extraData Additional data required by the callback function. 45 | */ 46 | error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData); 47 | 48 | /// @dev Revert queries for unimplemented resolver functions. 49 | error ResolverFunctionNotSupported(); 50 | 51 | /// @dev Revert if the recovered signer address is not an authorized signer. 52 | error InvalidSigner(); 53 | 54 | /*////////////////////////////////////////////////////////////// 55 | EVENTS 56 | //////////////////////////////////////////////////////////////*/ 57 | 58 | /** 59 | * @dev Emit an event when the contract owner authorizes a new signer. 60 | * 61 | * @param signer Address of the authorized signer. 62 | */ 63 | event AddSigner(address indexed signer); 64 | 65 | /** 66 | * @dev Emit an event when the contract owner removes an authorized signer. 67 | * 68 | * @param signer Address of the removed signer. 69 | */ 70 | event RemoveSigner(address indexed signer); 71 | 72 | /*////////////////////////////////////////////////////////////// 73 | CONSTANTS 74 | //////////////////////////////////////////////////////////////*/ 75 | 76 | /** 77 | * @dev Contract version specified using Farcaster protocol version scheme. 78 | */ 79 | string public constant VERSION = "2023.08.23"; 80 | 81 | /** 82 | * @dev EIP-712 typehash of the UsernameProof struct. 83 | */ 84 | bytes32 public constant USERNAME_PROOF_TYPEHASH = 85 | keccak256("UserNameProof(string name,uint256 timestamp,address owner)"); 86 | 87 | /*////////////////////////////////////////////////////////////// 88 | PARAMETERS 89 | //////////////////////////////////////////////////////////////*/ 90 | 91 | /** 92 | * @dev URL of the CCIP lookup gateway. 93 | */ 94 | string public url; 95 | 96 | /** 97 | * @dev Mapping of signer address to authorized boolean. 98 | */ 99 | mapping(address signer => bool isAuthorized) public signers; 100 | 101 | /*////////////////////////////////////////////////////////////// 102 | CONSTRUCTOR 103 | //////////////////////////////////////////////////////////////*/ 104 | 105 | /** 106 | * @notice Set the lookup gateway URL and initial signer. 107 | * 108 | * @param _url Lookup gateway URL. This value is set permanently. 109 | * @param _signer Initial authorized signer address. 110 | * @param _initialOwner Initial owner address. 111 | */ 112 | constructor( 113 | string memory _url, 114 | address _signer, 115 | address _initialOwner 116 | ) EIP712("Farcaster name verification", "1") { 117 | _transferOwnership(_initialOwner); 118 | url = _url; 119 | signers[_signer] = true; 120 | emit AddSigner(_signer); 121 | } 122 | 123 | /*////////////////////////////////////////////////////////////// 124 | RESOLVER VIEWS 125 | //////////////////////////////////////////////////////////////*/ 126 | 127 | /** 128 | * @notice Resolve the provided ENS name. This function will always revert to indicate an 129 | * offchain lookup. 130 | * 131 | * @param name DNS-encoded name to resolve. 132 | * @param data Encoded calldata of an ENS resolver function. This resolver supports only 133 | * address resolution (Signature 0x3b3b57de). Calling the CCIP gateway with any 134 | * other resolver function will revert. 135 | */ 136 | function resolve(bytes calldata name, bytes calldata data) external view returns (bytes memory) { 137 | if (bytes4(data[:4]) != IAddressQuery.addr.selector) { 138 | revert ResolverFunctionNotSupported(); 139 | } 140 | 141 | bytes memory callData = abi.encodeCall(IResolverService.resolve, (name, data)); 142 | string[] memory urls = new string[](1); 143 | urls[0] = url; 144 | 145 | revert OffchainLookup(address(this), urls, callData, this.resolveWithProof.selector, callData); 146 | } 147 | 148 | /** 149 | * @notice Offchain lookup callback. The caller must provide the signed response returned by 150 | * the lookup gateway. 151 | * 152 | * @param response Response from the CCIP gateway which has the following ABI-encoded fields: 153 | * - string: Fname of the username proof. 154 | * - uint256: Timestamp of the username proof. 155 | * - address: Owner address that signed the username proof. 156 | * - bytes: EIP-712 signature provided by the CCIP gateway server. 157 | * 158 | * @return ABI-encoded address of the fname owner. 159 | */ 160 | function resolveWithProof( 161 | bytes calldata response, 162 | bytes calldata /* extraData */ 163 | ) external view returns (bytes memory) { 164 | (string memory fname, uint256 timestamp, address fnameOwner, bytes memory signature) = 165 | abi.decode(response, (string, uint256, address, bytes)); 166 | 167 | bytes32 proofHash = 168 | keccak256(abi.encode(USERNAME_PROOF_TYPEHASH, keccak256(bytes(fname)), timestamp, fnameOwner)); 169 | bytes32 eip712hash = _hashTypedDataV4(proofHash); 170 | address signer = ECDSA.recover(eip712hash, signature); 171 | 172 | if (!signers[signer]) revert InvalidSigner(); 173 | 174 | return abi.encode(fnameOwner); 175 | } 176 | 177 | /*////////////////////////////////////////////////////////////// 178 | PERMISSIONED ACTIONS 179 | //////////////////////////////////////////////////////////////*/ 180 | 181 | /** 182 | * @notice Add a signer address to the authorized mapping. Only callable by owner. 183 | * 184 | * @param signer The signer address. 185 | */ 186 | function addSigner(address signer) external onlyOwner { 187 | signers[signer] = true; 188 | emit AddSigner(signer); 189 | } 190 | 191 | /** 192 | * @notice Remove a signer address from the authorized mapping. Only callable by owner. 193 | * 194 | * @param signer The signer address. 195 | */ 196 | function removeSigner(address signer) external onlyOwner { 197 | signers[signer] = false; 198 | emit RemoveSigner(signer); 199 | } 200 | 201 | /*////////////////////////////////////////////////////////////// 202 | INTERFACE DETECTION 203 | //////////////////////////////////////////////////////////////*/ 204 | 205 | function supportsInterface(bytes4 interfaceId) public view override returns (bool) { 206 | return interfaceId == type(IExtendedResolver).interfaceId || super.supportsInterface(interfaceId); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /script/DeployL2.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.21; 3 | 4 | import {StorageRegistry} from "../src/StorageRegistry.sol"; 5 | import {IdRegistry} from "../src/IdRegistry.sol"; 6 | import {IdGateway} from "../src/IdGateway.sol"; 7 | import {KeyRegistry} from "../src/KeyRegistry.sol"; 8 | import {KeyGateway} from "../src/KeyGateway.sol"; 9 | import {SignedKeyRequestValidator} from "../src/validators/SignedKeyRequestValidator.sol"; 10 | import {Bundler, IBundler} from "../src/Bundler.sol"; 11 | import {RecoveryProxy} from "../src/RecoveryProxy.sol"; 12 | import {IMetadataValidator} from "../src/interfaces/IMetadataValidator.sol"; 13 | import {console, ImmutableCreate2Deployer} from "./abstract/ImmutableCreate2Deployer.sol"; 14 | 15 | contract DeployL2 is ImmutableCreate2Deployer { 16 | uint256 public constant INITIAL_USD_UNIT_PRICE = 5e8; // $5 USD 17 | uint256 public constant INITIAL_MAX_UNITS = 200_000; 18 | uint256 public constant INITIAL_PRICE_FEED_CACHE_DURATION = 1 days; 19 | uint256 public constant INITIAL_UPTIME_FEED_GRACE_PERIOD = 1 hours; 20 | 21 | uint24 public constant KEY_REGISTRY_MIGRATION_GRACE_PERIOD = 1 days; 22 | uint256 public constant KEY_REGISTRY_MAX_KEYS_PER_FID = 1000; 23 | 24 | struct Salts { 25 | bytes32 storageRegistry; 26 | bytes32 idRegistry; 27 | bytes32 idGateway; 28 | bytes32 keyRegistry; 29 | bytes32 keyGateway; 30 | bytes32 signedKeyRequestValidator; 31 | bytes32 bundler; 32 | bytes32 recoveryProxy; 33 | } 34 | 35 | struct DeploymentParams { 36 | address initialIdRegistryOwner; 37 | address initialKeyRegistryOwner; 38 | address initialValidatorOwner; 39 | address initialRecoveryProxyOwner; 40 | address priceFeed; 41 | address uptimeFeed; 42 | address vault; 43 | address roleAdmin; 44 | address admin; 45 | address operator; 46 | address treasurer; 47 | address deployer; 48 | address migrator; 49 | Salts salts; 50 | } 51 | 52 | struct Addresses { 53 | address storageRegistry; 54 | address idRegistry; 55 | address idGateway; 56 | address keyRegistry; 57 | address keyGateway; 58 | address signedKeyRequestValidator; 59 | address bundler; 60 | address recoveryProxy; 61 | } 62 | 63 | struct Contracts { 64 | StorageRegistry storageRegistry; 65 | IdRegistry idRegistry; 66 | IdGateway idGateway; 67 | KeyRegistry keyRegistry; 68 | KeyGateway keyGateway; 69 | SignedKeyRequestValidator signedKeyRequestValidator; 70 | Bundler bundler; 71 | RecoveryProxy recoveryProxy; 72 | } 73 | 74 | function run() public { 75 | runSetup(runDeploy(loadDeploymentParams())); 76 | } 77 | 78 | function runDeploy(DeploymentParams memory params) public returns (Contracts memory) { 79 | return runDeploy(params, true); 80 | } 81 | 82 | function runDeploy(DeploymentParams memory params, bool broadcast) public returns (Contracts memory) { 83 | Addresses memory addrs; 84 | addrs.storageRegistry = register( 85 | "StorageRegistry", 86 | params.salts.storageRegistry, 87 | type(StorageRegistry).creationCode, 88 | abi.encode( 89 | params.priceFeed, 90 | params.uptimeFeed, 91 | INITIAL_USD_UNIT_PRICE, 92 | INITIAL_MAX_UNITS, 93 | params.vault, 94 | params.deployer, 95 | params.admin, 96 | params.operator, 97 | params.treasurer 98 | ) 99 | ); 100 | addrs.idRegistry = register( 101 | "IdRegistry", 102 | params.salts.idRegistry, 103 | type(IdRegistry).creationCode, 104 | abi.encode(params.migrator, params.deployer) 105 | ); 106 | addrs.idGateway = register( 107 | "IdGateway", 108 | params.salts.idGateway, 109 | type(IdGateway).creationCode, 110 | abi.encode(addrs.idRegistry, addrs.storageRegistry, params.deployer) 111 | ); 112 | addrs.keyRegistry = register( 113 | "KeyRegistry", 114 | params.salts.keyRegistry, 115 | type(KeyRegistry).creationCode, 116 | abi.encode(addrs.idRegistry, params.migrator, params.deployer, KEY_REGISTRY_MAX_KEYS_PER_FID) 117 | ); 118 | addrs.keyGateway = register( 119 | "KeyGateway", 120 | params.salts.keyGateway, 121 | type(KeyGateway).creationCode, 122 | abi.encode(addrs.keyRegistry, addrs.storageRegistry, params.initialKeyRegistryOwner) 123 | ); 124 | addrs.signedKeyRequestValidator = register( 125 | "SignedKeyRequestValidator", 126 | params.salts.signedKeyRequestValidator, 127 | type(SignedKeyRequestValidator).creationCode, 128 | abi.encode(addrs.idRegistry, params.initialValidatorOwner) 129 | ); 130 | addrs.bundler = register( 131 | "Bundler", params.salts.bundler, type(Bundler).creationCode, abi.encode(addrs.idGateway, addrs.keyGateway) 132 | ); 133 | addrs.recoveryProxy = register( 134 | "RecoveryProxy", 135 | params.salts.recoveryProxy, 136 | type(RecoveryProxy).creationCode, 137 | abi.encode(addrs.idRegistry, params.initialRecoveryProxyOwner) 138 | ); 139 | 140 | deploy(broadcast); 141 | 142 | return Contracts({ 143 | storageRegistry: StorageRegistry(addrs.storageRegistry), 144 | idRegistry: IdRegistry(addrs.idRegistry), 145 | idGateway: IdGateway(payable(addrs.idGateway)), 146 | keyRegistry: KeyRegistry(addrs.keyRegistry), 147 | keyGateway: KeyGateway(payable(addrs.keyGateway)), 148 | signedKeyRequestValidator: SignedKeyRequestValidator(addrs.signedKeyRequestValidator), 149 | bundler: Bundler(payable(addrs.bundler)), 150 | recoveryProxy: RecoveryProxy(addrs.recoveryProxy) 151 | }); 152 | } 153 | 154 | function runSetup(Contracts memory contracts, DeploymentParams memory params, bool broadcast) public { 155 | if (deploymentChanged()) { 156 | console.log("Running setup"); 157 | address bundler = address(contracts.bundler); 158 | 159 | if (broadcast) vm.startBroadcast(); 160 | contracts.idRegistry.setIdGateway(address(contracts.idGateway)); 161 | contracts.idRegistry.transferOwnership(params.initialIdRegistryOwner); 162 | 163 | contracts.idGateway.transferOwnership(params.initialIdRegistryOwner); 164 | 165 | contracts.keyRegistry.setValidator(1, 1, IMetadataValidator(address(contracts.signedKeyRequestValidator))); 166 | contracts.keyRegistry.setKeyGateway(address(contracts.keyGateway)); 167 | contracts.keyRegistry.transferOwnership(params.initialKeyRegistryOwner); 168 | 169 | contracts.storageRegistry.grantRole(keccak256("OPERATOR_ROLE"), bundler); 170 | contracts.storageRegistry.grantRole(0x00, params.roleAdmin); 171 | contracts.storageRegistry.renounceRole(0x00, params.deployer); 172 | if (broadcast) vm.stopBroadcast(); 173 | } else { 174 | console.log("No changes, skipping setup"); 175 | } 176 | } 177 | 178 | function runSetup(Contracts memory contracts) public { 179 | DeploymentParams memory params = loadDeploymentParams(); 180 | runSetup(contracts, params, true); 181 | } 182 | 183 | function loadDeploymentParams() internal returns (DeploymentParams memory) { 184 | return DeploymentParams({ 185 | initialIdRegistryOwner: vm.envAddress("ID_REGISTRY_OWNER_ADDRESS"), 186 | initialKeyRegistryOwner: vm.envAddress("KEY_REGISTRY_OWNER_ADDRESS"), 187 | initialValidatorOwner: vm.envAddress("METADATA_VALIDATOR_OWNER_ADDRESS"), 188 | initialRecoveryProxyOwner: vm.envAddress("RECOVERY_PROXY_OWNER_ADDRESS"), 189 | priceFeed: vm.envAddress("STORAGE_RENT_PRICE_FEED_ADDRESS"), 190 | uptimeFeed: vm.envAddress("STORAGE_RENT_UPTIME_FEED_ADDRESS"), 191 | vault: vm.envAddress("STORAGE_RENT_VAULT_ADDRESS"), 192 | roleAdmin: vm.envAddress("STORAGE_RENT_ROLE_ADMIN_ADDRESS"), 193 | admin: vm.envAddress("STORAGE_RENT_ADMIN_ADDRESS"), 194 | operator: vm.envAddress("STORAGE_RENT_OPERATOR_ADDRESS"), 195 | treasurer: vm.envAddress("STORAGE_RENT_TREASURER_ADDRESS"), 196 | deployer: vm.envAddress("DEPLOYER"), 197 | migrator: vm.envAddress("MIGRATOR_ADDRESS"), 198 | salts: Salts({ 199 | storageRegistry: vm.envOr("STORAGE_RENT_CREATE2_SALT", bytes32(0)), 200 | idRegistry: vm.envOr("ID_REGISTRY_CREATE2_SALT", bytes32(0)), 201 | idGateway: vm.envOr("ID_MANAGER_CREATE2_SALT", bytes32(0)), 202 | keyRegistry: vm.envOr("KEY_REGISTRY_CREATE2_SALT", bytes32(0)), 203 | keyGateway: vm.envOr("KEY_MANAGER_CREATE2_SALT", bytes32(0)), 204 | signedKeyRequestValidator: vm.envOr("SIGNED_KEY_REQUEST_VALIDATOR_CREATE2_SALT", bytes32(0)), 205 | bundler: vm.envOr("BUNDLER_CREATE2_SALT", bytes32(0)), 206 | recoveryProxy: vm.envOr("RECOVERY_PROXY_CREATE2_SALT", bytes32(0)) 207 | }) 208 | }); 209 | } 210 | } 211 | --------------------------------------------------------------------------------