├── .solhintignore ├── .commitlintrc.yml ├── .lintstagedrc.yml ├── audits └── NM_0411_0446_Worldcoin_Identity_Manager_v3_FINAL.pdf ├── .prettierignore ├── .prettierrc.yml ├── .env.example ├── .editorconfig ├── src ├── interfaces │ ├── IWorldIDIdentityManager.sol │ ├── IRootHistory.sol │ ├── IOpStateBridgeTransferOwnership.sol │ ├── ICrossDomainOwnable3.sol │ ├── ISemaphoreVerifier.sol │ ├── IPolygonWorldID.sol │ ├── IOpWorldID.sol │ └── IWorldID.sol ├── utils │ ├── SemaphoreTreeDepthValidator.sol │ └── BytesUtils.sol ├── test │ ├── data │ │ ├── InclusionProof.json │ │ ├── semaphore_16.json │ │ └── semaphore_30.json │ ├── MockStateBridge.t.sol │ ├── BytesUtils.t.sol │ ├── MockPolygonBridge.t.sol │ ├── CommonSemaphoreTest.t.sol │ ├── PolygonWorldID.t.sol │ ├── PolygonStateBridge.t.sol │ └── OpWorldID.t.sol ├── script │ ├── deploy │ │ ├── op-stack │ │ │ ├── DeployOpWorldID.s.sol │ │ │ ├── optimism │ │ │ │ ├── DeployOptimismStateBridgeDevnet.s.sol │ │ │ │ ├── DeployOptimismStateBridgeMainnet.s.sol │ │ │ │ └── DeployOptimismStateBridgeGoerli.s.sol │ │ │ └── base │ │ │ │ ├── DeployBaseStateBridgeMainnet.s.sol │ │ │ │ └── DeployBaseStateBridgeGoerli.s.sol │ │ ├── mock │ │ │ ├── DeployMockBridgedWorldID.s.sol │ │ │ ├── DeployMockWorldID.s.sol │ │ │ └── DeployMockStateBridge.s.sol │ │ └── polygon │ │ │ ├── DeployPolygonWorldIDMainnet.s.sol │ │ │ ├── DeployPolygonWorldIDMumbai.s.sol │ │ │ ├── DeployPolygonStateBridgeMainnet.s.sol │ │ │ └── DeployPolygonStateBridgeGoerli.s.sol │ ├── test │ │ └── PropagateMockRoot.s.sol │ ├── initialize │ │ ├── polygon │ │ │ └── InitializePolygonWorldID.s.sol │ │ └── op-stack │ │ │ ├── base │ │ │ ├── SetGasLimitBase.s.sol │ │ │ ├── LocalTransferOwnershipOfBaseWorldID.s.sol │ │ │ └── CrossTransferOwnershipOfBaseWorldID.s.sol │ │ │ └── optimism │ │ │ ├── SetGasLimitOptimism.s.sol │ │ │ ├── LocalTransferOwnershipOfOptimismWorldID.s.sol │ │ │ └── CrossTransferOwnershipOfOptimismWorldID.s.sol │ └── ownership │ │ ├── polygon │ │ └── InitializePolygonWorldID.s.sol │ │ ├── base │ │ ├── SetGasLimitBase.s.sol │ │ ├── LocalTransferOwnershipOfBaseWorldID.s.sol │ │ └── CrossTransferOwnershipOfBaseWorldID.s.sol │ │ └── optimism │ │ ├── SetGasLimitOptimism.s.sol │ │ ├── LocalTransferOwnershipOfOptimismWorldID.s.sol │ │ └── CrossTransferOwnershipOfOptimismWorldID.s.sol ├── mock │ ├── MockStateBridge.sol │ ├── MockBridgedWorldID.sol │ ├── MockPolygonBridge.sol │ └── MockWorldIDIdentityManager.sol ├── OpWorldID.sol ├── PolygonStateBridge.sol ├── PolygonWorldID.sol ├── OpStateBridge.sol └── abstract │ └── WorldIDBridge.sol ├── .solhint.json ├── .gitignore ├── .github └── workflows │ ├── relyance-sci.yml │ └── tests.yml ├── .gitmodules ├── LICENSE ├── .gas-snapshot ├── package.json ├── Makefile ├── docs └── spec.md ├── foundry.toml └── README.md /.solhintignore: -------------------------------------------------------------------------------- 1 | # directories 2 | **/lib 3 | **/node_modules 4 | -------------------------------------------------------------------------------- /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "@commitlint/config-conventional" 3 | -------------------------------------------------------------------------------- /.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | "*.{json,md,sol,yml}": 2 | - prettier --write 3 | -------------------------------------------------------------------------------- /audits/NM_0411_0446_Worldcoin_Identity_Manager_v3_FINAL.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcoin/world-id-state-bridge/HEAD/audits/NM_0411_0446_Worldcoin_Identity_Manager_v3_FINAL.pdf -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .yarn/ 3 | **/broadcast 4 | **/cache 5 | **/lib 6 | **/out 7 | **/node_modules 8 | 9 | # Solidity 10 | *.sol 11 | 12 | # files 13 | *.env 14 | *.log 15 | .pnp.* 16 | coverage.json 17 | yarn-debug.log* 18 | yarn-error.log* 19 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | bracketSpacing: true 2 | printWidth: 120 3 | proseWrap: "always" 4 | singleQuote: false 5 | tabWidth: 2 6 | trailingComma: "all" 7 | useTabs: false 8 | 9 | overrides: 10 | - files: "*.sol" 11 | options: 12 | compiler: "0.8.17" 13 | tabWidth: 4 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # run: source .env to load env vars 2 | 3 | # Copy this file without the .example postfix and uncomment one of the options below 4 | 5 | ## Use a custom RPC 6 | # MAINNET_RPC_URL=YOUR_CUSTOM_RPC_URL 7 | 8 | ## Use Llama RPC 9 | # MAINNET_RPC_URL=https://eth.llamarpc.com 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # All files 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.sol] 16 | indent_size = 4 17 | 18 | [*.tree] 19 | indent_size = 1 20 | -------------------------------------------------------------------------------- /src/interfaces/IWorldIDIdentityManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | /// @title IWorldIDIdentityManager 5 | /// @author Worldcoin 6 | /// @dev used to fetch the latest root from the WorldIDIdentityManager 7 | interface IWorldIDIdentityManager { 8 | /// @notice returns the latest root 9 | function latestRoot() external view returns (uint256); 10 | } 11 | -------------------------------------------------------------------------------- /.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 | "prettier/prettier": [ 11 | "error", 12 | { 13 | "endOfLine": "auto" 14 | } 15 | ], 16 | "reason-string": ["warn", { "maxLength": 64 }] 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 | node_modules 12 | .vscode 13 | 14 | docs/src/* 15 | docs/book/* 16 | docs/book.css 17 | docs/book.toml 18 | docs/solidity.min.js 19 | docs/.gitignore 20 | 21 | # files 22 | *.env 23 | *.log 24 | src/script/.deploy-config.json 25 | .DS_Store 26 | .pnp.* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # broadcasts 31 | !/broadcast 32 | /broadcast/* 33 | /broadcast/*/31337/ 34 | 35 | # remappings 36 | remappings.txt -------------------------------------------------------------------------------- /.github/workflows/relyance-sci.yml: -------------------------------------------------------------------------------- 1 | name: Relyance SCI Scan 2 | 3 | on: 4 | schedule: 5 | - cron: "2 0 * * *" 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | execute-relyance-sci: 13 | name: Relyance SCI Job 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | 18 | steps: 19 | - name: Run Relyance SCI 20 | uses: worldcoin/gh-actions-public/relyance@main 21 | # More information: https://github.com/worldcoin/gh-actions-public/tree/main/relyance 22 | with: 23 | secrets-dpp-sci-key: ${{ secrets.DPP_SCI_KEY }} 24 | -------------------------------------------------------------------------------- /src/utils/SemaphoreTreeDepthValidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.15; 3 | 4 | /// @title Semaphore tree depth validator 5 | /// @author Worldcoin 6 | library SemaphoreTreeDepthValidator { 7 | /// @notice Checks if the provided `treeDepth` is among supported depths. 8 | /// 9 | /// @param treeDepth The tree depth to validate. 10 | /// @return supportedDepth Returns `true` if `treeDepth` is between 16 and 32 11 | function validate(uint8 treeDepth) internal pure returns (bool supportedDepth) { 12 | uint8 minDepth = 16; 13 | uint8 maxDepth = 32; 14 | return treeDepth >= minDepth && treeDepth <= maxDepth; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | branch = v1.2.0 3 | path = lib/forge-std 4 | url = https://github.com/foundry-rs/forge-std 5 | [submodule "lib/prb-test"] 6 | branch = v0.3.1 7 | path = lib/prb-test 8 | url = https://github.com/paulrberg/prb-test 9 | [submodule "lib/openzeppelin-contracts"] 10 | path = lib/openzeppelin-contracts 11 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 12 | branch = v4.8.0 13 | [submodule "lib/openzeppelin-contracts-upgradeable"] 14 | path = lib/openzeppelin-contracts-upgradeable 15 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable 16 | branch = v4.8.1 17 | [submodule "lib/solmate"] 18 | path = lib/solmate 19 | url = https://github.com/transmissions11/solmate 20 | -------------------------------------------------------------------------------- /src/interfaces/IRootHistory.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | /// @title Interface for WorldID setRooHistoryExpiry 5 | /// @author Worldcoin 6 | /// @notice Interface for WorldID setRooHistoryExpiry 7 | /// @dev Used in StateBridge to set the root history expiry time on Optimism (OPWorldID) 8 | /// @custom:usage abi.encodeCall(IRootHistory.setRootHistoryExpiry, (_expiryTime)); 9 | interface IRootHistory { 10 | /// @notice Sets the amount of time it takes for a root in the root history to expire. 11 | /// 12 | /// @param expiryTime The new amount of time it takes for a root to expire. 13 | /// 14 | /// @custom:reverts string If the caller is not the owner. 15 | function setRootHistoryExpiry(uint256 expiryTime) external; 16 | } 17 | -------------------------------------------------------------------------------- /src/interfaces/IOpStateBridgeTransferOwnership.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.15; 2 | 3 | /// @title OpStateBridgeTransferOwnership 4 | /// @notice Interface for the StateBridge to transfer ownership 5 | /// of OpWorldID to another contract on L1 or to a OP Stack chain EOA or contract 6 | /// @dev This is a subset of the OpStateBridge contract 7 | abstract contract IOpStateBridgeTransferOwnership { 8 | /// @notice Adds functionality to the StateBridge to transfer ownership 9 | /// of OpWorldID to another contract on L1 or to a local OP Stack chain EOA 10 | /// @param _owner new owner (EOA or contract) 11 | /// @param _isLocal true if new owner is on Optimism, false if it is a cross-domain owner 12 | function transferOwnershipOp(address _owner, bool _isLocal) external virtual; 13 | } 14 | -------------------------------------------------------------------------------- /src/interfaces/ICrossDomainOwnable3.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.15; 2 | 3 | /// @title Optimism - CrossDomainOwnable3 Interface 4 | /// @author Worldcoin 5 | /// @notice Interface for the CrossDomainOwnable contract for the Optimism L2 6 | /// @dev Adds functionality to the StateBridge to transfer ownership 7 | /// of OpWorldID to another contract on L1 or to a local Optimism EOA 8 | /// @custom:usage abi.encodeCall(ICrossDomainOwnable3.transferOwnership, (_owner, _isLocal)); 9 | interface ICrossDomainOwnable3 { 10 | /// @notice transfers owner to a cross-domain or local owner 11 | /// @param _owner new owner (EOA or contract) 12 | /// @param _isLocal true if new owner is on Optimism, false if it is a cross-domain owner 13 | function transferOwnership(address _owner, bool _isLocal) external; 14 | } 15 | -------------------------------------------------------------------------------- /src/interfaces/ISemaphoreVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | /// @title Tree Verifier Interface 5 | /// @author Worldcoin 6 | /// @notice An interface representing a merkle tree verifier. 7 | interface ISemaphoreVerifier { 8 | /// @notice Verify an uncompressed Groth16 proof. 9 | /// @notice Reverts with InvalidProof if the proof is invalid or 10 | /// with PublicInputNotInField the public input is not reduced. 11 | /// @notice There is no return value. If the function does not revert, the 12 | /// proof was succesfully verified. 13 | /// @param proof the points (A, B, C) in EIP-197 format matching the output 14 | /// of compressProof. 15 | /// @param input the public input field elements in the scalar field Fr. 16 | /// Elements must be reduced. 17 | function verifyProof(uint256[8] calldata proof, uint256[4] calldata input) external view; 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | name: Unit Tests 9 | 10 | jobs: 11 | tests: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | submodules: recursive 18 | 19 | - name: Install Foundry 20 | uses: onbjerg/foundry-toolchain@v1 21 | with: 22 | version: stable 23 | 24 | - uses: actions/setup-node@v2 25 | with: 26 | node-version: "18" 27 | cache: "npm" 28 | 29 | - name: Install Dependencies 30 | run: make install 31 | 32 | - name: Build Contracts 33 | run: make build 34 | 35 | - name: Check formatting 36 | run: make format-check 37 | 38 | - name: Run Tests 39 | env: 40 | MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} 41 | run: make test 42 | -------------------------------------------------------------------------------- /src/interfaces/IPolygonWorldID.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | /// @title Interface for the PolygonWorldID contract 5 | /// @author Worldcoin 6 | /// @notice Interface for the CrossDomainOwnable contract for the Optimism L2 7 | /// @custom:usage abi.encodeCall(IPolygonWorldID.receiveRoot, (_newRoot, _supersedeTimestamp)); 8 | interface IPolygonWorldID { 9 | //////////////////////////////////////////////////////////////////////////////// 10 | /// ROOT MIRRORING /// 11 | /////////////////////////////////////////////////////////////////////////////// 12 | 13 | /// @notice This function is called by the state bridge contract when it forwards a new root to 14 | /// the bridged WorldID. 15 | /// 16 | /// @param newRoot The value of the new root. 17 | /// 18 | /// @custom:reverts CannotOverwriteRoot If the root already exists in the root history. 19 | /// @custom:reverts string If the caller is not the owner. 20 | function receiveRoot(uint256 newRoot) external; 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Worldcoin Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /src/test/data/InclusionProof.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "0xdf9f0cb5a3afe2129e349c1435bfbe9e6f091832fdfa7b739b61c5db2cbdde9", 3 | "signal_hash": "0xbc6bb462e38af7da48e0ae7b5cbae860141c04e5af2cf92328cd6548df111f", 4 | "external_nullifier_hash": "0xfd3a1e9736c12a5d4a31f26362b577ccafbd523d358daf40cdc04d90e17f77", 5 | "nullifier_hash": "0x2887375654a2f83868b277f3836678aa55475fd5c840b117913ea4a7c9ded6fc", 6 | "proof": [ 7 | [ 8 | "0x12bca28c242e87a12637280e957ee6eefa29446e11cd4e49f86df9fd320fbdba", 9 | "0x1f399a7953de230a6da3a70e49c284b60ee5040cd71ee668994f70f49ff73489" 10 | ], 11 | [ 12 | [ 13 | "0x9b28c42a4b34507ae0ae61f72342888b64a2f89c1c45e2e897a53ee2b946406", 14 | "0x28a5825073e62d01c9f5c8172664349180df8a09d68026a29df91500964a0a72" 15 | ], 16 | [ 17 | "0x24356f7d8e4565514fc8a575834b6b37bd7df399ae99424187f30591557e4c4a", 18 | "0x177dc415a6d6e866c0125a0f73f41e7a5b291f18a90864e5fbe106c67df82f67" 19 | ] 20 | ], 21 | [ 22 | "0x1fb872833d2373abaa26cca40f498b878cef8134cae18cc703e68f99b9938fa5", 23 | "0x1487bc571a30416ad9124f5f8ef9e2efe5a0d720a710f6abf0095004f250b306" 24 | ] 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/script/deploy/op-stack/DeployOpWorldID.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {OpWorldID} from "src/OpWorldID.sol"; 6 | 7 | /// @title OpWorldID deployment script 8 | /// @notice forge script to deploy OpWorldID.sol to an OP Stack chain 9 | /// @author Worldcoin 10 | /// @dev Can be executed by running `make mock`, `make deploy` or `make deploy-testnet`. 11 | contract DeployOpWorldID is Script { 12 | OpWorldID public opWorldID; 13 | 14 | /////////////////////////////////////////////////////////////////// 15 | /// CONFIG /// 16 | /////////////////////////////////////////////////////////////////// 17 | string public root = vm.projectRoot(); 18 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 19 | string public json = vm.readFile(path); 20 | 21 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 22 | uint8 public treeDepth = uint8(30); 23 | 24 | function run() external { 25 | vm.startBroadcast(privateKey); 26 | 27 | opWorldID = new OpWorldID(treeDepth); 28 | 29 | vm.stopBroadcast(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/script/deploy/mock/DeployMockBridgedWorldID.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {MockBridgedWorldID} from "src/mock/MockBridgedWorldID.sol"; 6 | 7 | /// @title MockBridgedWorldID deployment script 8 | /// @notice forge script to deploy MockBridgedWorldID.sol 9 | /// @author Worldcoin 10 | /// @dev Can be executed by running `make local-mock`. 11 | contract DeployMockBridgedWorldID is Script { 12 | MockBridgedWorldID public mockBridgedWorldID; 13 | 14 | /////////////////////////////////////////////////////////////////// 15 | /// CONFIG /// 16 | /////////////////////////////////////////////////////////////////// 17 | string public root = vm.projectRoot(); 18 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 19 | string public json = vm.readFile(path); 20 | 21 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 22 | uint8 public treeDepth = abi.decode(vm.parseJson(json, ".treeDepth"), (uint8)); 23 | 24 | function run() external { 25 | vm.startBroadcast(privateKey); 26 | 27 | mockBridgedWorldID = new MockBridgedWorldID(treeDepth); 28 | 29 | vm.stopBroadcast(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/script/test/PropagateMockRoot.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {MockStateBridge} from "src/mock/MockStateBridge.sol"; 6 | 7 | /// @title Propagate Mock Root test script 8 | /// @author Worldcoin 9 | /// @dev Can be executed by running `make local-mock`. 10 | contract PropagateMockRoot is Script { 11 | MockStateBridge public bridge; 12 | 13 | address public mockStateBridgeAddress; 14 | 15 | /////////////////////////////////////////////////////////////////// 16 | /// CONFIG /// 17 | /////////////////////////////////////////////////////////////////// 18 | string public root = vm.projectRoot(); 19 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 20 | string public json = vm.readFile(path); 21 | 22 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 23 | 24 | function setUp() public { 25 | mockStateBridgeAddress = 26 | abi.decode(vm.parseJson(json, ".mockStateBridgeAddress"), (address)); 27 | } 28 | 29 | function run() public { 30 | vm.startBroadcast(privateKey); 31 | 32 | bridge = MockStateBridge(mockStateBridgeAddress); 33 | 34 | bridge.propagateRoot(); 35 | 36 | vm.stopBroadcast(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gas-snapshot: -------------------------------------------------------------------------------- 1 | OpWorldIDTest:testCanGetTreeDepth(uint8) (runs: 256, μ: 7028149, ~: 7028149) 2 | OpWorldIDTest:testConstructorWithInvalidTreeDepth(uint8) (runs: 256, μ: 6353485, ~: 6353508) 3 | OpWorldIDTest:test_expiredRoot_reverts(uint256,uint256) (runs: 256, μ: 181262, ~: 181262) 4 | OpWorldIDTest:test_onlyOwner_notMessenger_reverts(uint256) (runs: 256, μ: 28627, ~: 28627) 5 | OpWorldIDTest:test_onlyOwner_notOwner_reverts(uint256) (runs: 256, μ: 32311, ~: 32311) 6 | OpWorldIDTest:test_receiveVerifyInvalidRoot_reverts(uint256) (runs: 256, μ: 121696, ~: 122894) 7 | OpWorldIDTest:test_receiveVerifyRoot_succeeds(uint256) (runs: 256, μ: 117021, ~: 118219) 8 | PolygonWorldIDTest:testCanGetTreeDepth(uint8) (runs: 256, μ: 7017617, ~: 7017617) 9 | PolygonWorldIDTest:testConstructorWithInvalidTreeDepth(uint8) (runs: 256, μ: 6333463, ~: 6333486) 10 | StateBridgeTest:test_canSelectFork_succeeds() (gas: 5683) 11 | StateBridgeTest:test_notOwner_transferOwnershipOptimism_reverts(address,address,bool) (runs: 256, μ: 13944, ~: 13944) 12 | StateBridgeTest:test_notOwner_transferOwnership_reverts(address,address) (runs: 256, μ: 13745, ~: 13745) 13 | StateBridgeTest:test_owner_transferOwnershipOptimism_succeeds(address,bool) (runs: 256, μ: 117814, ~: 117814) 14 | StateBridgeTest:test_owner_transferOwnership_succeeds(address) (runs: 256, μ: 20748, ~: 20748) 15 | StateBridgeTest:test_sendRootMultichain_reverts(uint256,uint256) (runs: 256, μ: 178455, ~: 178455) 16 | StateBridgeTest:test_sendRootMultichain_succeeds(uint256) (runs: 256, μ: 173073, ~: 173073) -------------------------------------------------------------------------------- /src/interfaces/IOpWorldID.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | /// @title Interface for the OpWorldID contract 5 | /// @author Worldcoin 6 | /// @custom:usage abi.encodeCall(IOpWorldID.receiveRoot, (_newRoot, _supersedeTimestamp)); 7 | interface IOpWorldID { 8 | //////////////////////////////////////////////////////////////////////////////// 9 | /// ROOT MIRRORING /// 10 | /////////////////////////////////////////////////////////////////////////////// 11 | 12 | /// @notice This function is called by the state bridge contract when it forwards a new root to 13 | /// the bridged WorldID. 14 | /// @dev This function can revert if Optimism's CrossDomainMessenger stops processing proofs 15 | /// or if OPLabs stops submitting them. Next iteration of Optimism's cross-domain messaging, will be 16 | /// fully permissionless for message-passing, so this will not be an issue. 17 | /// Sequencer needs to include changes to the CrossDomainMessenger contract on L1, not economically penalized 18 | /// if messages are not included, however the fraud prover (Cannon) can force the sequencer to include it. 19 | /// 20 | /// @param newRoot The value of the new root. 21 | /// 22 | /// @custom:reverts CannotOverwriteRoot If the root already exists in the root history. 23 | /// @custom:reverts string If the caller is not the owner. 24 | function receiveRoot(uint256 newRoot) external; 25 | } 26 | -------------------------------------------------------------------------------- /src/interfaces/IWorldID.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | /// @title WorldID Interface 5 | /// @author Worldcoin 6 | /// @notice The interface to the Semaphore Groth16 proof verification for WorldID. 7 | /// @custom:usage IWorldID(worldIDIdentityMangerAddress).verifyProof(root, signalHash, nullifierHash, externalNullifierHash, proof); 8 | interface IWorldID { 9 | /// @notice Verifies a WorldID zero knowledge proof. 10 | /// @dev Note that a double-signaling check is not included here, and should be carried by the 11 | /// caller. 12 | /// @dev It is highly recommended that the implementation is restricted to `view` if possible. 13 | /// 14 | /// @param root The of the Merkle tree 15 | /// @param signalHash A keccak256 hash of the Semaphore signal 16 | /// @param nullifierHash The nullifier hash 17 | /// @param externalNullifierHash A keccak256 hash of the external nullifier 18 | /// @param proof The zero-knowledge proof 19 | /// 20 | /// @custom:reverts string If the `proof` is invalid. 21 | function verifyProof( 22 | uint256 root, 23 | uint256 signalHash, 24 | uint256 nullifierHash, 25 | uint256 externalNullifierHash, 26 | uint256[8] calldata proof 27 | ) external; 28 | 29 | function verifyCompressedProof( 30 | uint256 root, 31 | uint256 signalHash, 32 | uint256 nullifierHash, 33 | uint256 externalNullifierHash, 34 | uint256[4] calldata compressedProof 35 | ) external view; 36 | } 37 | -------------------------------------------------------------------------------- /src/script/deploy/mock/DeployMockWorldID.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | /// @dev Demo deployments 5 | /// @custom:deployment Goerli 0x0a4501cda7d0decb737376f0efbb692b4922bc56 6 | /// @custom:link https://goerli.etherscan.io/address/0x0a4501cda7d0decb737376f0efbb692b4922bc56 7 | import {Script} from "forge-std/Script.sol"; 8 | import {MockWorldIDIdentityManager} from "src/mock/MockWorldIDIdentityManager.sol"; 9 | 10 | /// @title Mock World ID deployment script 11 | /// @notice forge script to deploy MockWorldIDIdentityManager.sol 12 | /// @author Worldcoin 13 | /// @dev Can be executed by running `make mock` or `make local-mock`. 14 | contract DeployMockWorldID is Script { 15 | MockWorldIDIdentityManager public worldID; 16 | 17 | /////////////////////////////////////////////////////////////////// 18 | /// CONFIG /// 19 | /////////////////////////////////////////////////////////////////// 20 | string public root = vm.projectRoot(); 21 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 22 | string public json = vm.readFile(path); 23 | 24 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 25 | uint256 public sampleRoot = abi.decode(vm.parseJson(json, ".sampleRoot"), (uint256)); 26 | 27 | function run() external { 28 | vm.startBroadcast(privateKey); 29 | 30 | worldID = new MockWorldIDIdentityManager(sampleRoot); 31 | 32 | vm.stopBroadcast(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/script/deploy/polygon/DeployPolygonWorldIDMainnet.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {PolygonWorldID} from "src/PolygonWorldID.sol"; 6 | 7 | /// @title PolygonWorldID deployment script on Polygon PoS mainnet 8 | /// @notice forge script to deploy PolygonWorldID.sol 9 | /// @author Worldcoin 10 | /// @dev Can be executed by running `make mock`, `make deploy` or `make deploy-testnet`. 11 | contract DeployPolygonWorldID is Script { 12 | address public stateBridgeAddress; 13 | 14 | // Polygon PoS Mainnet Child Tunnel 15 | address public fxChildAddress = address(0x8397259c983751DAf40400790063935a11afa28a); 16 | 17 | PolygonWorldID public polygonWorldId; 18 | uint256 public privateKey; 19 | uint8 public treeDepth; 20 | 21 | /////////////////////////////////////////////////////////////////// 22 | /// CONFIG /// 23 | /////////////////////////////////////////////////////////////////// 24 | string public root = vm.projectRoot(); 25 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 26 | string public json = vm.readFile(path); 27 | 28 | function setUp() public { 29 | privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 30 | treeDepth = uint8(30); 31 | } 32 | 33 | function run() external { 34 | vm.startBroadcast(privateKey); 35 | 36 | polygonWorldId = new PolygonWorldID(treeDepth, fxChildAddress); 37 | 38 | vm.stopBroadcast(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/script/deploy/polygon/DeployPolygonWorldIDMumbai.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {PolygonWorldID} from "src/PolygonWorldID.sol"; 6 | 7 | /// @title PolygonWorldID deployment script on Polygon Mumbai 8 | /// @notice forge script to deploy PolygonWorldID.sol 9 | /// @author Worldcoin 10 | /// @dev Can be executed by running `make mock`, `make deploy` or `make deploy-testnet`. 11 | contract DeployPolygonWorldIDMumbai is Script { 12 | address public stateBridgeAddress; 13 | 14 | // Polygon PoS Mumbai Testnet Child Tunnel 15 | address public fxChildAddress = address(0xCf73231F28B7331BBe3124B907840A94851f9f11); 16 | 17 | PolygonWorldID public polygonWorldId; 18 | uint256 public privateKey; 19 | 20 | uint8 public treeDepth; 21 | 22 | /////////////////////////////////////////////////////////////////// 23 | /// CONFIG /// 24 | /////////////////////////////////////////////////////////////////// 25 | string public root = vm.projectRoot(); 26 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 27 | string public json = vm.readFile(path); 28 | 29 | function setUp() public { 30 | privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 31 | treeDepth = uint8(30); 32 | } 33 | 34 | function run() external { 35 | vm.startBroadcast(privateKey); 36 | 37 | polygonWorldId = new PolygonWorldID(treeDepth, fxChildAddress); 38 | 39 | vm.stopBroadcast(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@worldcoin/world-id-state-bridge", 3 | "version": "0.1.0", 4 | "description": "State bridge between the WorldID Ethereum mainnet deployment and WorldID supported networks", 5 | "license": "MIT", 6 | "type": "module", 7 | "files": [ 8 | "src/**/*.sol" 9 | ], 10 | "engines": { 11 | "node": ">=18.0.0 <=22.0.0" 12 | }, 13 | "keywords": [ 14 | "blockchain", 15 | "ethereum", 16 | "smart-contracts", 17 | "semaphore", 18 | "identity", 19 | "solidity", 20 | "zero-knowledge", 21 | "proof-of-uniqueness", 22 | "bridge" 23 | ], 24 | "homepage": "https://github.com/worldcoin/world-id-state-bridge", 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/worldcoin/world-id-state-bridge.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/worldcoin/world-id-state-bridge/issues" 31 | }, 32 | "dependencies": { 33 | "@eth-optimism/contracts": "^0.6.0", 34 | "@eth-optimism/contracts-bedrock": "^0.13.2", 35 | "@eth-optimism/sdk": "^1.10.4", 36 | "alchemy-sdk": "^2.9.2", 37 | "commander": "^10.0.1", 38 | "dotenv": "^16.3.1", 39 | "ethers": "^6.6.5", 40 | "ora": "^6.3.1", 41 | "prettier": "^2.8.7", 42 | "solhint": "^3.6.1" 43 | }, 44 | "scripts": { 45 | "format": "forge fmt", 46 | "lint": "yarn lint:sol && yarn prettier:check", 47 | "lint:sol": "forge fmt --check && yarn solhint \"{script,src,test}/**/*.sol\"", 48 | "prettier:check": "prettier --check \"**/*.{json,md,yml}\"", 49 | "prettier:write": "prettier --write \"**/*.{json,md,yml}\"" 50 | }, 51 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" 52 | } 53 | -------------------------------------------------------------------------------- /src/script/initialize/polygon/InitializePolygonWorldID.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | // Demo deployments 5 | 6 | import {Script} from "forge-std/Script.sol"; 7 | import {PolygonWorldID} from "src/PolygonWorldID.sol"; 8 | 9 | contract InitializePolygonWorldID is Script { 10 | address public stateBridgeAddress; 11 | address public polygonWorldIDAddress; 12 | 13 | // Polygon PoS Mumbai Testnet Child Tunnel 14 | address public fxChildAddress = address(0xCf73231F28B7331BBe3124B907840A94851f9f11); 15 | 16 | PolygonWorldID public polygonWorldID; 17 | uint256 public privateKey; 18 | 19 | /////////////////////////////////////////////////////////////////// 20 | /// CONFIG /// 21 | /////////////////////////////////////////////////////////////////// 22 | string public root = vm.projectRoot(); 23 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 24 | string public json = vm.readFile(path); 25 | 26 | function setUp() public { 27 | privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 28 | 29 | stateBridgeAddress = abi.decode(vm.parseJson(json, ".polygonStateBridgeAddress"), (address)); 30 | polygonWorldIDAddress = abi.decode(vm.parseJson(json, ".polygonWorldIDAddress"), (address)); 31 | } 32 | 33 | // Polygon PoS Mainnet Child Tunnel 34 | // address fxChildAddress = address(0x8397259c983751DAf40400790063935a11afa28a); 35 | 36 | function run() external { 37 | vm.startBroadcast(privateKey); 38 | 39 | polygonWorldID = PolygonWorldID(polygonWorldIDAddress); 40 | 41 | polygonWorldID.setFxRootTunnel(stateBridgeAddress); 42 | 43 | vm.stopBroadcast(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/script/ownership/polygon/InitializePolygonWorldID.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | // Demo deployments 5 | 6 | import {Script} from "forge-std/Script.sol"; 7 | import {PolygonWorldID} from "src/PolygonWorldID.sol"; 8 | 9 | contract InitializePolygonWorldID is Script { 10 | address public polygonStateBridgeAddress; 11 | address public polygonWorldIDAddress; 12 | 13 | // Polygon PoS Mumbai Testnet Child Tunnel 14 | address public fxChildAddress = address(0xCf73231F28B7331BBe3124B907840A94851f9f11); 15 | 16 | PolygonWorldID public polygonWorldID; 17 | uint256 public privateKey; 18 | 19 | /////////////////////////////////////////////////////////////////// 20 | /// CONFIG /// 21 | /////////////////////////////////////////////////////////////////// 22 | string public root = vm.projectRoot(); 23 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 24 | string public json = vm.readFile(path); 25 | 26 | function setUp() public { 27 | privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 28 | 29 | polygonStateBridgeAddress = 30 | abi.decode(vm.parseJson(json, ".polygonStateBridgeAddress"), (address)); 31 | polygonWorldIDAddress = abi.decode(vm.parseJson(json, ".polygonWorldIDAddress"), (address)); 32 | } 33 | 34 | // Polygon PoS Mainnet Child Tunnel 35 | // address fxChildAddress = address(0x8397259c983751DAf40400790063935a11afa28a); 36 | 37 | function run() external { 38 | vm.startBroadcast(privateKey); 39 | 40 | polygonWorldID = PolygonWorldID(polygonWorldIDAddress); 41 | 42 | polygonWorldID.setFxRootTunnel(polygonStateBridgeAddress); 43 | 44 | vm.stopBroadcast(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/script/deploy/mock/DeployMockStateBridge.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {MockWorldIDIdentityManager} from "src/mock/MockWorldIDIdentityManager.sol"; 6 | import {MockBridgedWorldID} from "src/mock/MockBridgedWorldID.sol"; 7 | import {MockStateBridge} from "src/mock/MockStateBridge.sol"; 8 | 9 | /// @title Mock State Bridge deployment script 10 | /// @notice forge script to deploy MockStateBridge.sol 11 | /// @author Worldcoin 12 | /// @dev Can be executed by running `make mock` or `make local-mock`. 13 | contract DeployMockStateBridge is Script { 14 | MockStateBridge public mockStateBridge; 15 | MockWorldIDIdentityManager public mockWorldID; 16 | MockBridgedWorldID public mockBridgedWorldID; 17 | 18 | address owner; 19 | 20 | uint8 treeDepth; 21 | 22 | uint256 initialRoot; 23 | 24 | /////////////////////////////////////////////////////////////////// 25 | /// CONFIG /// 26 | /////////////////////////////////////////////////////////////////// 27 | string public root = vm.projectRoot(); 28 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 29 | string public json = vm.readFile(path); 30 | 31 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 32 | 33 | function setUp() public {} 34 | 35 | function run() public { 36 | vm.startBroadcast(privateKey); 37 | 38 | treeDepth = uint8(30); 39 | 40 | initialRoot = uint256(0x111); 41 | 42 | mockBridgedWorldID = new MockBridgedWorldID(treeDepth); 43 | mockWorldID = new MockWorldIDIdentityManager(initialRoot); 44 | mockStateBridge = new MockStateBridge(address(mockWorldID), address(mockBridgedWorldID)); 45 | 46 | mockStateBridge.propagateRoot(); 47 | 48 | vm.stopBroadcast(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/mock/MockStateBridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {MockBridgedWorldID} from "./MockBridgedWorldID.sol"; 5 | import {IWorldIDIdentityManager} from "src/interfaces/IWorldIDIdentityManager.sol"; 6 | import {Ownable} from "openzeppelin-contracts/access/Ownable.sol"; 7 | import {IWorldIDIdentityManager} from "../interfaces/IWorldIDIdentityManager.sol"; 8 | 9 | /// @title Mock State Bridge 10 | /// @author Worldcoin 11 | /// @notice Mock of the StateBridge to test functionality on a local chain 12 | /// @custom:deployment deployed through make local-mock 13 | contract MockStateBridge is Ownable { 14 | /// @notice MockWorldIDIdentityManager contract which will hold a mock root 15 | IWorldIDIdentityManager public worldID; 16 | 17 | /// @notice MockBridgedWorldID contract which will receive the root 18 | MockBridgedWorldID public mockBridgedWorldID; 19 | 20 | /// @notice Emmited when the root is not a valid root in the canonical WorldID Identity Manager contract 21 | error InvalidRoot(); 22 | 23 | /// @notice constructor 24 | constructor(address _mockWorldID, address _mockBridgedWorldID) { 25 | worldID = IWorldIDIdentityManager(_mockWorldID); 26 | mockBridgedWorldID = MockBridgedWorldID(_mockBridgedWorldID); 27 | } 28 | 29 | /// @notice Sends the latest WorldID Identity Manager root to the Bridged WorldID contract. 30 | /// @dev Calls this method on the L1 Proxy contract to relay roots to WorldID supported chains. 31 | function propagateRoot() public { 32 | uint256 latestRoot = worldID.latestRoot(); 33 | _sendRootToMockBridgedWorldID(latestRoot); 34 | } 35 | 36 | // @notice Sends the latest WorldID Identity Manager root to all chains. 37 | /// @dev Calls this method on the L1 Proxy contract to relay roots to WorldID supported chains. 38 | /// @param root The latest WorldID Identity Manager root. 39 | function _sendRootToMockBridgedWorldID(uint256 root) internal { 40 | mockBridgedWorldID.receiveRoot(root); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/script/deploy/op-stack/optimism/DeployOptimismStateBridgeDevnet.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {OpStateBridge} from "src/OpStateBridge.sol"; 6 | 7 | /// @title Deploy State Bridge Optimism 8 | /// @notice forge script to deploy OpStateBridge.sol on Ethereum mainnet 9 | /// @author Worldcoin 10 | contract DeployOpStateBridgeDevnet is Script { 11 | OpStateBridge public bridge; 12 | 13 | address public opWorldIDAddress; 14 | address public worldIDIdentityManagerAddress; 15 | address public opCrossDomainMessengerAddress; 16 | 17 | /////////////////////////////////////////////////////////////////// 18 | /// CONFIG /// 19 | /////////////////////////////////////////////////////////////////// 20 | string public root = vm.projectRoot(); 21 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 22 | string public json = vm.readFile(path); 23 | 24 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 25 | 26 | function setUp() public { 27 | /////////////////////////////////////////////////////////////////// 28 | /// OPTIMISM /// 29 | /////////////////////////////////////////////////////////////////// 30 | opCrossDomainMessengerAddress = address(0x7E75b00FfBF0a4295ab7112F04Fd8255334194BD); 31 | 32 | /////////////////////////////////////////////////////////////////// 33 | /// WORLD ID /// 34 | /////////////////////////////////////////////////////////////////// 35 | worldIDIdentityManagerAddress = 36 | abi.decode(vm.parseJson(json, ".worldIDIdentityManagerAddress"), (address)); 37 | opWorldIDAddress = abi.decode(vm.parseJson(json, ".optimismWorldIDAddress"), (address)); 38 | } 39 | 40 | function run() public { 41 | vm.startBroadcast(privateKey); 42 | 43 | bridge = new OpStateBridge( 44 | worldIDIdentityManagerAddress, opWorldIDAddress, opCrossDomainMessengerAddress 45 | ); 46 | 47 | vm.stopBroadcast(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/script/deploy/op-stack/optimism/DeployOptimismStateBridgeMainnet.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {OpStateBridge} from "src/OpStateBridge.sol"; 6 | 7 | /// @title Deploy State Bridge Optimism 8 | /// @notice forge script to deploy OpStateBridge.sol on Ethereum mainnet 9 | /// @author Worldcoin 10 | /// @dev Can be executed by running `make mock`, `make deploy` or `make deploy-testnet`. 11 | contract DeployOpStateBridgeMainnet is Script { 12 | OpStateBridge public bridge; 13 | 14 | address public opWorldIDAddress; 15 | address public worldIDIdentityManagerAddress; 16 | address public opCrossDomainMessengerAddress; 17 | 18 | /////////////////////////////////////////////////////////////////// 19 | /// CONFIG /// 20 | /////////////////////////////////////////////////////////////////// 21 | string public root = vm.projectRoot(); 22 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 23 | string public json = vm.readFile(path); 24 | 25 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 26 | 27 | function setUp() public { 28 | /////////////////////////////////////////////////////////////////// 29 | /// OPTIMISM /// 30 | /////////////////////////////////////////////////////////////////// 31 | opCrossDomainMessengerAddress = address(0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1); 32 | 33 | /////////////////////////////////////////////////////////////////// 34 | /// WORLD ID /// 35 | /////////////////////////////////////////////////////////////////// 36 | worldIDIdentityManagerAddress = 37 | abi.decode(vm.parseJson(json, ".worldIDIdentityManagerAddress"), (address)); 38 | opWorldIDAddress = abi.decode(vm.parseJson(json, ".optimismWorldIDAddress"), (address)); 39 | } 40 | 41 | function run() public { 42 | vm.startBroadcast(privateKey); 43 | 44 | bridge = new OpStateBridge( 45 | worldIDIdentityManagerAddress, opWorldIDAddress, opCrossDomainMessengerAddress 46 | ); 47 | 48 | vm.stopBroadcast(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/MockStateBridge.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.15; 2 | 3 | import {MockStateBridge} from "src/mock/MockStateBridge.sol"; 4 | import {MockWorldIDIdentityManager} from "src/mock/MockWorldIDIdentityManager.sol"; 5 | import {MockBridgedWorldID} from "src/mock/MockBridgedWorldID.sol"; 6 | import {PRBTest} from "@prb/test/PRBTest.sol"; 7 | import {StdCheats} from "forge-std/StdCheats.sol"; 8 | 9 | /// @title Mock State Bridge Test 10 | /// @author Worldcoin 11 | contract MockStateBridgeTest is PRBTest, StdCheats { 12 | MockStateBridge public mockStateBridge; 13 | MockWorldIDIdentityManager public mockWorldID; 14 | MockBridgedWorldID public mockBridgedWorldID; 15 | 16 | address public owner; 17 | 18 | uint8 public treeDepth; 19 | 20 | uint256 public initialRoot; 21 | 22 | /// @notice The time in the `rootHistory` mapping associated with a root that has never been 23 | /// seen before. 24 | uint128 internal constant NULL_ROOT_TIME = 0; 25 | 26 | /// @notice Emitted when root history expiry is set 27 | event RootHistoryExpirySet(uint256 rootHistoryExpiry); 28 | 29 | /// @notice Emitted when a new root is received by the contract. 30 | /// 31 | /// @param root The value of the root that was added. 32 | /// @param timestamp The timestamp of insertion for the given root. 33 | event RootAdded(uint256 root, uint128 timestamp); 34 | 35 | function setUp() public { 36 | owner = address(0x1234); 37 | 38 | vm.label(owner, "owner"); 39 | 40 | treeDepth = uint8(30); 41 | 42 | initialRoot = uint256(0x111); 43 | 44 | vm.prank(owner); 45 | mockBridgedWorldID = new MockBridgedWorldID(treeDepth); 46 | 47 | vm.prank(owner); 48 | mockWorldID = new MockWorldIDIdentityManager(initialRoot); 49 | 50 | vm.prank(owner); 51 | mockStateBridge = new MockStateBridge(address(mockWorldID), address(mockBridgedWorldID)); 52 | 53 | vm.prank(owner); 54 | mockBridgedWorldID.transferOwnership(address(mockStateBridge)); 55 | } 56 | 57 | function testPropagateRootSucceeds() public { 58 | vm.expectEmit(true, true, true, true); 59 | emit RootAdded(initialRoot, uint128(block.timestamp)); 60 | 61 | mockStateBridge.propagateRoot(); 62 | 63 | assert(mockWorldID.latestRoot() == mockBridgedWorldID.latestRoot()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # By default we want to build. 2 | all: install build 3 | 4 | # ===== Basic Development Rules ======================================================================================= 5 | 6 | # Install forge dependencies (not needed if submodules are already initialized). 7 | install:; forge install && yarn install 8 | 9 | # Build contracts and inject the Poseidon library. 10 | build:; forge build 11 | 12 | # Run tests, with debug information and gas reports. 13 | test:; FOUNDRY_PROFILE=debug forge test 14 | 15 | # ===== Profiling Rules =============================================================================================== 16 | 17 | # Benchmark the tests. 18 | bench:; FOUNDRY_PROFILE=bench forge test --gas-report 19 | 20 | # Snapshot the current test usages. 21 | snapshot:; FOUNDRY_PROFILE=bench forge snapshot 22 | 23 | # ===== Deployment Rules ============================================================================================== 24 | 25 | # Deploy contracts 26 | deploy: install build; node --no-warnings src/script/deploy.js deploy 27 | 28 | deploy-testnet: install build; node --no-warnings src/script/deploy.js deploy-testnet 29 | 30 | deploy-devnet: install build; node --no-warnings src/script/deploy.js deploy-devnet 31 | 32 | mock: install build; node --no-warnings src/script/deploy.js mock 33 | 34 | local-mock: install build; node --no-warnings src/script/deploy.js local-mock 35 | 36 | set-op-gas-limit: install build; node --no-warnings src/script/deploy.js set-op-gas-limit 37 | 38 | # Upgrade contracts 39 | # upgrade: install build; node --no-warnings scripts/deploy.js upgrade 40 | 41 | # ===== Utility Rules ================================================================================================= 42 | 43 | # Format the solidity code. 44 | format:; forge fmt; npx prettier --write . 45 | 46 | # Checks the formatting 47 | format-check:; forge fmt --check; npx prettier --check . 48 | 49 | # Lint the solidity code. 50 | lint:; yarn lint 51 | 52 | # Clean the build artifacts. 53 | clean:; forge clean 54 | 55 | # Get a test coverage report. 56 | coverage:; forge coverage 57 | 58 | # Update forge dependencies. 59 | update:; forge update 60 | 61 | # ===== Documentation Rules ============================================================================================ 62 | 63 | # Generate the documentation. 64 | doc:; forge doc --build; forge doc --serve -p 3000 65 | -------------------------------------------------------------------------------- /src/script/ownership/base/SetGasLimitBase.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {OpStateBridge} from "src/OpStateBridge.sol"; 6 | 7 | /// @title Base State Bridge Gas Limit setter 8 | /// @notice forge script to set the correct gas limits for crossDomainMessenger calls to Base for the StateBridge 9 | /// @author Worldcoin 10 | /// @dev Can be executed by running `make set-base-gas-limit` 11 | contract SetOpGasLimitBase is Script { 12 | address public baseStateBridgeAddress; 13 | 14 | OpStateBridge public baseStateBridge; 15 | 16 | /////////////////////////////////////////////////////////////////// 17 | /// CONFIG /// 18 | /////////////////////////////////////////////////////////////////// 19 | string public root = vm.projectRoot(); 20 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 21 | string public json = vm.readFile(path); 22 | 23 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 24 | 25 | /////////////////////////////////////////////////////////////////// 26 | /// OP GAS LIMITS /// 27 | /////////////////////////////////////////////////////////////////// 28 | uint32 public gasLimitSendRootBase = 29 | abi.decode(vm.parseJson(json, ".gasLimitSendRootBase"), (uint32)); 30 | uint32 public gasLimitSetRootHistoryExpiryBase = 31 | abi.decode(vm.parseJson(json, ".gasLimitSetRootHistoryExpiryBase"), (uint32)); 32 | uint32 public gasLimitTransferOwnershipBase = 33 | abi.decode(vm.parseJson(json, ".gasLimitTransferOwnershipBase"), (uint32)); 34 | 35 | function setUp() public { 36 | baseStateBridgeAddress = 37 | abi.decode(vm.parseJson(json, ".baseStateBridgeAddress"), (address)); 38 | 39 | baseStateBridge = OpStateBridge(baseStateBridgeAddress); 40 | } 41 | 42 | function run() public { 43 | vm.startBroadcast(privateKey); 44 | 45 | baseStateBridge.setGasLimitPropagateRoot(gasLimitSendRootBase); 46 | baseStateBridge.setGasLimitSetRootHistoryExpiry(gasLimitSetRootHistoryExpiryBase); 47 | baseStateBridge.setGasLimitTransferOwnershipOp(gasLimitTransferOwnershipBase); 48 | 49 | vm.stopBroadcast(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/script/initialize/op-stack/base/SetGasLimitBase.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {OpStateBridge} from "src/OpStateBridge.sol"; 6 | 7 | /// @title Base State Bridge Gas Limit setter 8 | /// @notice forge script to set the correct gas limits for crossDomainMessenger calls to Base for the StateBridge 9 | /// @author Worldcoin 10 | /// @dev Can be executed by running `make set-base-gas-limit` 11 | contract SetOpGasLimitBase is Script { 12 | address public baseStateBridgeAddress; 13 | 14 | OpStateBridge public baseStateBridge; 15 | 16 | /////////////////////////////////////////////////////////////////// 17 | /// CONFIG /// 18 | /////////////////////////////////////////////////////////////////// 19 | string public root = vm.projectRoot(); 20 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 21 | string public json = vm.readFile(path); 22 | 23 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 24 | 25 | /////////////////////////////////////////////////////////////////// 26 | /// OP GAS LIMITS /// 27 | /////////////////////////////////////////////////////////////////// 28 | uint32 public gasLimitSendRootBase = 29 | abi.decode(vm.parseJson(json, ".gasLimitSendRootBase"), (uint32)); 30 | uint32 public gasLimitSetRootHistoryExpiryBase = 31 | abi.decode(vm.parseJson(json, ".gasLimitSetRootHistoryExpiryBase"), (uint32)); 32 | uint32 public gasLimitTransferOwnershipBase = 33 | abi.decode(vm.parseJson(json, ".gasLimitTransferOwnershipBase"), (uint32)); 34 | 35 | function setUp() public { 36 | baseStateBridgeAddress = 37 | abi.decode(vm.parseJson(json, ".baseStateBridgeAddress"), (address)); 38 | 39 | baseStateBridge = OpStateBridge(baseStateBridgeAddress); 40 | } 41 | 42 | function run() public { 43 | vm.startBroadcast(privateKey); 44 | 45 | baseStateBridge.setGasLimitPropagateRoot(gasLimitSendRootBase); 46 | baseStateBridge.setGasLimitSetRootHistoryExpiry(gasLimitSetRootHistoryExpiryBase); 47 | baseStateBridge.setGasLimitTransferOwnershipOp(gasLimitTransferOwnershipBase); 48 | 49 | vm.stopBroadcast(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/script/deploy/op-stack/optimism/DeployOptimismStateBridgeGoerli.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | /// @dev Demo deployments 5 | import {Script} from "forge-std/Script.sol"; 6 | import {OpStateBridge} from "src/OpStateBridge.sol"; 7 | 8 | /// @title Optimism State Bridge deployment script 9 | /// @notice forge script to deploy StateBridge.sol on Optimism 10 | /// @author Worldcoin 11 | /// @dev Can be executed by running `make mock`, `make local-mock`, `make deploy` or `make deploy-testnet`. 12 | contract DeployOpStateBridgeGoerli is Script { 13 | OpStateBridge public bridge; 14 | 15 | address public opWorldIDAddress; 16 | address public worldIDIdentityManagerAddress; 17 | address public opCrossDomainMessengerAddress; 18 | 19 | /////////////////////////////////////////////////////////////////// 20 | /// CONFIG /// 21 | /////////////////////////////////////////////////////////////////// 22 | string public root = vm.projectRoot(); 23 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 24 | string public json = vm.readFile(path); 25 | 26 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 27 | 28 | function setUp() public { 29 | /////////////////////////////////////////////////////////////////// 30 | /// OPTIMISM /// 31 | /////////////////////////////////////////////////////////////////// 32 | opCrossDomainMessengerAddress = address(0x5086d1eEF304eb5284A0f6720f79403b4e9bE294); 33 | 34 | /////////////////////////////////////////////////////////////////// 35 | /// WORLD ID /// 36 | /////////////////////////////////////////////////////////////////// 37 | worldIDIdentityManagerAddress = 38 | abi.decode(vm.parseJson(json, ".worldIDIdentityManagerAddress"), (address)); 39 | opWorldIDAddress = abi.decode(vm.parseJson(json, ".optimismWorldIDAddress"), (address)); 40 | } 41 | 42 | function run() public { 43 | vm.startBroadcast(privateKey); 44 | 45 | bridge = new OpStateBridge( 46 | worldIDIdentityManagerAddress, opWorldIDAddress, opCrossDomainMessengerAddress 47 | ); 48 | 49 | vm.stopBroadcast(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/script/deploy/op-stack/base/DeployBaseStateBridgeMainnet.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {OpStateBridge} from "src/OpStateBridge.sol"; 6 | 7 | /// @title Deploy Base State Bridge 8 | /// @notice forge script to deploy OpStateBridge contract for Base 9 | /// @author Worldcoin 10 | /// @dev Can be executed by running `make mock`, `make deploy` or `make deploy-testnet`. 11 | contract DeployBaseStateBridgeMainnet is Script { 12 | OpStateBridge public bridge; 13 | 14 | address public baseWorldIDAddress; 15 | address public worldIDIdentityManagerAddress; 16 | address public baseCrossDomainMessengerAddress; 17 | 18 | /////////////////////////////////////////////////////////////////// 19 | /// CONFIG /// 20 | /////////////////////////////////////////////////////////////////// 21 | string public root = vm.projectRoot(); 22 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 23 | string public json = vm.readFile(path); 24 | 25 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 26 | 27 | function setUp() public { 28 | /////////////////////////////////////////////////////////////////// 29 | /// BASE /// 30 | /////////////////////////////////////////////////////////////////// 31 | // Taken from https://docs.base.org/base-contracts 32 | baseCrossDomainMessengerAddress = address(0x866E82a600A1414e583f7F13623F1aC5d58b0Afa); 33 | 34 | /////////////////////////////////////////////////////////////////// 35 | /// WORLD ID /// 36 | /////////////////////////////////////////////////////////////////// 37 | worldIDIdentityManagerAddress = 38 | abi.decode(vm.parseJson(json, ".worldIDIdentityManagerAddress"), (address)); 39 | baseWorldIDAddress = abi.decode(vm.parseJson(json, ".baseWorldIDAddress"), (address)); 40 | } 41 | 42 | function run() public { 43 | vm.startBroadcast(privateKey); 44 | 45 | bridge = new OpStateBridge( 46 | worldIDIdentityManagerAddress, baseWorldIDAddress, baseCrossDomainMessengerAddress 47 | ); 48 | 49 | vm.stopBroadcast(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/script/deploy/op-stack/base/DeployBaseStateBridgeGoerli.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {OpStateBridge} from "src/OpStateBridge.sol"; 6 | 7 | /// @title Base State Bridge deployment script 8 | /// @notice forge script to deploy OpStateBridge.sol on Base 9 | /// @author Worldcoin 10 | /// @dev Can be executed by running `make mock`, `make local-mock`, `make deploy` or `make deploy-testnet`. 11 | contract DeployBaseStateBridgeGoerli is Script { 12 | OpStateBridge public bridge; 13 | 14 | address public baseWorldIDAddress; 15 | address public worldIDIdentityManagerAddress; 16 | address public baseCrossDomainMessengerAddress; 17 | 18 | /////////////////////////////////////////////////////////////////// 19 | /// CONFIG /// 20 | /////////////////////////////////////////////////////////////////// 21 | string public root = vm.projectRoot(); 22 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 23 | string public json = vm.readFile(path); 24 | 25 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 26 | 27 | function setUp() public { 28 | /////////////////////////////////////////////////////////////////// 29 | /// BASE /// 30 | /////////////////////////////////////////////////////////////////// 31 | // Taken from https://docs.base.org/base-contracts 32 | baseCrossDomainMessengerAddress = address(0x8e5693140eA606bcEB98761d9beB1BC87383706D); 33 | 34 | /////////////////////////////////////////////////////////////////// 35 | /// WORLD ID /// 36 | /////////////////////////////////////////////////////////////////// 37 | worldIDIdentityManagerAddress = 38 | abi.decode(vm.parseJson(json, ".worldIDIdentityManagerAddress"), (address)); 39 | baseWorldIDAddress = abi.decode(vm.parseJson(json, ".baseWorldIDAddress"), (address)); 40 | } 41 | 42 | function run() public { 43 | vm.startBroadcast(privateKey); 44 | 45 | bridge = new OpStateBridge( 46 | worldIDIdentityManagerAddress, baseWorldIDAddress, baseCrossDomainMessengerAddress 47 | ); 48 | 49 | vm.stopBroadcast(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/script/ownership/base/LocalTransferOwnershipOfBaseWorldID.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {ICrossDomainOwnable3} from "src/interfaces/ICrossDomainOwnable3.sol"; 6 | import {OpStateBridge} from "src/OpStateBridge.sol"; 7 | 8 | /// @title Ownership Transfer of OpWorldID on Base 9 | /// @notice forge script for transferring ownership of OpWorldID to a local (Base / Base Goerli) 10 | /// or cross-chain (Ethereum / Ethereum goerli) EOA or contract 11 | /// @author Worldcoin 12 | /// @dev Can be executed by running `make mock`, `make local-mock`, `make deploy` or `make deploy-testnet`. 13 | contract LocalTransferOwnershipOfBaseWorldID is Script { 14 | uint256 public privateKey; 15 | 16 | address public baseWorldIDAddress; 17 | 18 | address public newOwner; 19 | 20 | /// @notice in CrossDomainOwnable3.sol, isLocal is used to set ownership to a new address with a toggle 21 | /// for local or cross domain (using the CrossDomainMessenger to pass messages) 22 | bool public isLocal; 23 | 24 | uint32 public opGasLimit; 25 | 26 | function setUp() public { 27 | /////////////////////////////////////////////////////////////////// 28 | /// CONFIG /// 29 | /////////////////////////////////////////////////////////////////// 30 | string memory root = vm.projectRoot(); 31 | string memory path = string.concat(root, "/src/script/.deploy-config.json"); 32 | string memory json = vm.readFile(path); 33 | 34 | privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 35 | baseWorldIDAddress = abi.decode(vm.parseJson(json, ".baseWorldIDAddress"), (address)); 36 | newOwner = abi.decode(vm.parseJson(json, ".newBaseWorldIDOwner"), (address)); 37 | } 38 | 39 | constructor() {} 40 | 41 | function run() public { 42 | /// @notice cross domain ownership flag 43 | /// false = cross domain (address on Ethereum) 44 | /// true = local (address on Optimism) 45 | isLocal = false; 46 | 47 | vm.startBroadcast(privateKey); 48 | 49 | bytes memory call = 50 | abi.encodeCall(ICrossDomainOwnable3.transferOwnership, (newOwner, isLocal)); 51 | 52 | (bool ok,) = baseWorldIDAddress.call(call); 53 | 54 | require(ok, "call failed"); 55 | 56 | vm.stopBroadcast(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/script/initialize/op-stack/base/LocalTransferOwnershipOfBaseWorldID.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {ICrossDomainOwnable3} from "src/interfaces/ICrossDomainOwnable3.sol"; 6 | import {OpStateBridge} from "src/OpStateBridge.sol"; 7 | 8 | /// @title Ownership Transfer of OpWorldID on Base 9 | /// @notice forge script for transferring ownership of OpWorldID to a local (Base / Base Goerli) 10 | /// or cross-chain (Ethereum / Ethereum goerli) EOA or contract 11 | /// @author Worldcoin 12 | /// @dev Can be executed by running `make mock`, `make local-mock`, `make deploy` or `make deploy-testnet`. 13 | contract LocalTransferOwnershipOfBaseWorldID is Script { 14 | uint256 public privateKey; 15 | 16 | address public baseWorldIDAddress; 17 | 18 | address public newOwner; 19 | 20 | /// @notice in CrossDomainOwnable3.sol, isLocal is used to set ownership to a new address with a toggle 21 | /// for local or cross domain (using the CrossDomainMessenger to pass messages) 22 | bool public isLocal; 23 | 24 | uint32 public opGasLimit; 25 | 26 | function setUp() public { 27 | /////////////////////////////////////////////////////////////////// 28 | /// CONFIG /// 29 | /////////////////////////////////////////////////////////////////// 30 | string memory root = vm.projectRoot(); 31 | string memory path = string.concat(root, "/src/script/.deploy-config.json"); 32 | string memory json = vm.readFile(path); 33 | 34 | privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 35 | baseWorldIDAddress = abi.decode(vm.parseJson(json, ".baseWorldIDAddress"), (address)); 36 | newOwner = abi.decode(vm.parseJson(json, ".baseStateBridgeAddress"), (address)); 37 | } 38 | 39 | constructor() {} 40 | 41 | function run() public { 42 | /// @notice cross domain ownership flag 43 | /// false = cross domain (address on Ethereum) 44 | /// true = local (address on Optimism) 45 | isLocal = false; 46 | 47 | vm.startBroadcast(privateKey); 48 | 49 | bytes memory call = 50 | abi.encodeCall(ICrossDomainOwnable3.transferOwnership, (newOwner, isLocal)); 51 | 52 | (bool ok,) = baseWorldIDAddress.call(call); 53 | 54 | require(ok, "call failed"); 55 | 56 | vm.stopBroadcast(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/script/ownership/optimism/SetGasLimitOptimism.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {OpStateBridge} from "src/OpStateBridge.sol"; 6 | 7 | /// @title Optimism State Bridge Gas Limit setter 8 | /// @notice forge script to set the correct gas limits for crossDomainMessenger calls to Optimism for the StateBridge 9 | /// @author Worldcoin 10 | /// @dev Can be executed by running `make set-op-gas-limit` 11 | contract SetOpGasLimitOptimism is Script { 12 | address public optimismStateBridgeAddress; 13 | 14 | OpStateBridge public optimismStateBridge; 15 | 16 | /////////////////////////////////////////////////////////////////// 17 | /// CONFIG /// 18 | /////////////////////////////////////////////////////////////////// 19 | string public root = vm.projectRoot(); 20 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 21 | string public json = vm.readFile(path); 22 | 23 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 24 | 25 | /////////////////////////////////////////////////////////////////// 26 | /// OP GAS LIMITS /// 27 | /////////////////////////////////////////////////////////////////// 28 | uint32 public gasLimitSendRootOptimism = 29 | abi.decode(vm.parseJson(json, ".gasLimitSendRootOptimism"), (uint32)); 30 | uint32 public gasLimitSetRootHistoryExpiryOptimism = 31 | abi.decode(vm.parseJson(json, ".gasLimitSetRootHistoryExpiryOptimism"), (uint32)); 32 | uint32 public gasLimitTransferOwnershipOptimism = 33 | abi.decode(vm.parseJson(json, ".gasLimitTransferOwnershipOptimism"), (uint32)); 34 | 35 | function setUp() public { 36 | optimismStateBridgeAddress = 37 | abi.decode(vm.parseJson(json, ".optimismStateBridgeAddress"), (address)); 38 | 39 | optimismStateBridge = OpStateBridge(optimismStateBridgeAddress); 40 | } 41 | 42 | function run() public { 43 | vm.startBroadcast(privateKey); 44 | 45 | optimismStateBridge.setGasLimitPropagateRoot(gasLimitSendRootOptimism); 46 | optimismStateBridge.setGasLimitSetRootHistoryExpiry(gasLimitSetRootHistoryExpiryOptimism); 47 | optimismStateBridge.setGasLimitTransferOwnershipOp(gasLimitTransferOwnershipOptimism); 48 | 49 | vm.stopBroadcast(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/script/initialize/op-stack/optimism/SetGasLimitOptimism.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {OpStateBridge} from "src/OpStateBridge.sol"; 6 | 7 | /// @title Optimism State Bridge Gas Limit setter 8 | /// @notice forge script to set the correct gas limits for crossDomainMessenger calls to Optimism for the StateBridge 9 | /// @author Worldcoin 10 | /// @dev Can be executed by running `make set-op-gas-limit` 11 | contract SetOpGasLimitOptimism is Script { 12 | address public optimismStateBridgeAddress; 13 | 14 | OpStateBridge public optimismStateBridge; 15 | 16 | /////////////////////////////////////////////////////////////////// 17 | /// CONFIG /// 18 | /////////////////////////////////////////////////////////////////// 19 | string public root = vm.projectRoot(); 20 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 21 | string public json = vm.readFile(path); 22 | 23 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 24 | 25 | /////////////////////////////////////////////////////////////////// 26 | /// OP GAS LIMITS /// 27 | /////////////////////////////////////////////////////////////////// 28 | uint32 public gasLimitSendRootOptimism = 29 | abi.decode(vm.parseJson(json, ".gasLimitSendRootOptimism"), (uint32)); 30 | uint32 public gasLimitSetRootHistoryExpiryOptimism = 31 | abi.decode(vm.parseJson(json, ".gasLimitSetRootHistoryExpiryOptimism"), (uint32)); 32 | uint32 public gasLimitTransferOwnershipOptimism = 33 | abi.decode(vm.parseJson(json, ".gasLimitTransferOwnershipOptimism"), (uint32)); 34 | 35 | function setUp() public { 36 | optimismStateBridgeAddress = 37 | abi.decode(vm.parseJson(json, ".optimismStateBridgeAddress"), (address)); 38 | 39 | optimismStateBridge = OpStateBridge(optimismStateBridgeAddress); 40 | } 41 | 42 | function run() public { 43 | vm.startBroadcast(privateKey); 44 | 45 | optimismStateBridge.setGasLimitPropagateRoot(gasLimitSendRootOptimism); 46 | optimismStateBridge.setGasLimitSetRootHistoryExpiry(gasLimitSetRootHistoryExpiryOptimism); 47 | optimismStateBridge.setGasLimitTransferOwnershipOp(gasLimitTransferOwnershipOptimism); 48 | 49 | vm.stopBroadcast(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/script/ownership/optimism/LocalTransferOwnershipOfOptimismWorldID.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {ICrossDomainOwnable3} from "src/interfaces/ICrossDomainOwnable3.sol"; 6 | import {OpStateBridge} from "src/OpStateBridge.sol"; 7 | 8 | /// @title Ownership Transfer of OpWorldID script for Optimism 9 | /// @notice forge script for transferring ownership of OpWorldID to a local (Optimism) 10 | /// or cross-chain (Ethereum Goerli) EOA or contract 11 | /// @author Worldcoin 12 | /// @dev Can be executed by running `make mock`, `make local-mock`, `make deploy` or `make deploy-testnet`. 13 | contract LocalTransferOwnershipOfOptimismWorldID is Script { 14 | uint256 public privateKey; 15 | 16 | address public optimismWorldIDAddress; 17 | 18 | address public newOwner; 19 | 20 | /// @notice in CrossDomainOwnable3.sol, isLocal is used to set ownership to a new address with a toggle 21 | /// for local or cross domain (using the CrossDomainMessenger to pass messages) 22 | bool public isLocal; 23 | 24 | uint32 public opGasLimit; 25 | 26 | function setUp() public { 27 | /////////////////////////////////////////////////////////////////// 28 | /// CONFIG /// 29 | /////////////////////////////////////////////////////////////////// 30 | string memory root = vm.projectRoot(); 31 | string memory path = string.concat(root, "/src/script/.deploy-config.json"); 32 | string memory json = vm.readFile(path); 33 | 34 | privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 35 | optimismWorldIDAddress = 36 | abi.decode(vm.parseJson(json, ".optimismWorldIDAddress"), (address)); 37 | newOwner = abi.decode(vm.parseJson(json, ".newOptimismWorldIDOwner"), (address)); 38 | } 39 | 40 | constructor() {} 41 | 42 | function run() public { 43 | /// @notice cross domain ownership flag 44 | /// false = cross domain (address on Ethereum) 45 | /// true = local (address on Optimism) 46 | isLocal = false; 47 | 48 | vm.startBroadcast(privateKey); 49 | 50 | bytes memory call = 51 | abi.encodeCall(ICrossDomainOwnable3.transferOwnership, (newOwner, isLocal)); 52 | 53 | (bool ok,) = optimismWorldIDAddress.call(call); 54 | 55 | require(ok, "call failed"); 56 | 57 | vm.stopBroadcast(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/script/initialize/op-stack/optimism/LocalTransferOwnershipOfOptimismWorldID.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {ICrossDomainOwnable3} from "src/interfaces/ICrossDomainOwnable3.sol"; 6 | import {OpStateBridge} from "src/OpStateBridge.sol"; 7 | 8 | /// @title Ownership Transfer of OpWorldID script for Optimism 9 | /// @notice forge script for transferring ownership of OpWorldID to a local (Optimism) 10 | /// or cross-chain (Ethereum Goerli) EOA or contract 11 | /// @author Worldcoin 12 | /// @dev Can be executed by running `make mock`, `make local-mock`, `make deploy` or `make deploy-testnet`. 13 | contract LocalTransferOwnershipOfOptimismWorldID is Script { 14 | uint256 public privateKey; 15 | 16 | address public optimismWorldIDAddress; 17 | 18 | address public newOwner; 19 | 20 | /// @notice in CrossDomainOwnable3.sol, isLocal is used to set ownership to a new address with a toggle 21 | /// for local or cross domain (using the CrossDomainMessenger to pass messages) 22 | bool public isLocal; 23 | 24 | uint32 public opGasLimit; 25 | 26 | function setUp() public { 27 | /////////////////////////////////////////////////////////////////// 28 | /// CONFIG /// 29 | /////////////////////////////////////////////////////////////////// 30 | string memory root = vm.projectRoot(); 31 | string memory path = string.concat(root, "/src/script/.deploy-config.json"); 32 | string memory json = vm.readFile(path); 33 | 34 | privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 35 | optimismWorldIDAddress = 36 | abi.decode(vm.parseJson(json, ".optimismWorldIDAddress"), (address)); 37 | newOwner = abi.decode(vm.parseJson(json, ".optimismStateBridgeAddress"), (address)); 38 | } 39 | 40 | constructor() {} 41 | 42 | function run() public { 43 | /// @notice cross domain ownership flag 44 | /// false = cross domain (address on Ethereum) 45 | /// true = local (address on Optimism) 46 | isLocal = false; 47 | 48 | vm.startBroadcast(privateKey); 49 | 50 | bytes memory call = 51 | abi.encodeCall(ICrossDomainOwnable3.transferOwnership, (newOwner, isLocal)); 52 | 53 | (bool ok,) = optimismWorldIDAddress.call(call); 54 | 55 | require(ok, "call failed"); 56 | 57 | vm.stopBroadcast(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/spec.md: -------------------------------------------------------------------------------- 1 | # State Bridge spec 2 | 3 | ![state-bridge.svg](state-bridge.svg) 4 | 5 | Propagates new World ID merkle tree roots from the `WorldIDIdentityManager` contract 6 | ([`world-id-contracts`](https://github.com/worldcoin/world-id-contracts) repo) on Ethereum mainnet to Optimism, Base and 7 | Polygon PoS (there is also a staging environment on Goerli testnets, more info in [deployments.md](./deployments.md)). 8 | 9 | Currently there are 3 different state bridges which support Optimism, Base and Polygon PoS respectively. World ID merkle 10 | tree roots are queried from the `WorldIDIdentityManager` contracts using the `latestRoot()` public method and propagated 11 | to their respective chain using the `propagateRoot()` method. 12 | 13 | ## Polygon PoS state transfer infrastructure 14 | 15 | In order to bridge state from Ethereum to Polygon the `PolygonStateBridge.sol` contract is currently using the 16 | [FxPortal contracts](https://wiki.polygon.technology/docs/pos/design/bridge/l1-l2-communication/fx-portal/). It takes 17 | about 20-40 minutes to sync the state from the state bridge to the `PolygonWorldID.sol` contract and about 1 hour to 18 | checkpoint said state to the Polygon bridge on L1. 19 | 20 | ## Optimism L1<>L2 infrastructure 21 | 22 | The `OpStateBridge.sol` contract uses the OP Stack chain native bridge contract `L1CrossDomainMessenger` and 23 | `L2CrossDomainMessenger` to relay messages from L1 to L2. A guide can be found in the 24 | [Optimism documentation](https://community.optimism.io/docs/developers/bridge/messaging/) to learn more about how this 25 | mechanism works. 26 | 27 | Assumption: Optimism Bridge currently relies on OP labs submitting output commitments, however they are working on 28 | making it fully permissionless so that anyone can submit their own output commitment. The L2 node is used to fetch the 29 | latest state root from the L1 contract and submits it to the L2 contract. So if we want to send a message to the 30 | messenger on L1, that triggers a change in the canonical Optimism state that the Optimism sequencer has to include as a 31 | transaction on the L2. That state transition goes through the fault proof mechanism so that if its not included in the 32 | L2, the sequencer can be forced to include it by the fault proof mechanism (even economically punished in future 33 | iterations of the protocol). Current time to relay message (estimation for Bedrock) is around 2 minutes for a message 34 | from L1 to L2. Worst case scenario can be up to a couple of hours if sequencer is malicious and willingly doesn't want 35 | to include the transition. 36 | -------------------------------------------------------------------------------- /src/script/ownership/base/CrossTransferOwnershipOfBaseWorldID.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {OpWorldID} from "src/OpWorldID.sol"; 6 | import {ICrossDomainOwnable3} from "src/interfaces/ICrossDomainOwnable3.sol"; 7 | import {IOpStateBridgeTransferOwnership} from "src/interfaces/IOpStateBridgeTransferOwnership.sol"; 8 | import {OpStateBridge} from "src/OpStateBridge.sol"; 9 | 10 | /// @title Ownership Transfer of OpWorldID on Base script 11 | /// @notice forge script for transferring ownership of OpWorldID to a local (Base) 12 | /// or cross-chain (Ethereum) EOA or contract 13 | /// @author Worldcoin 14 | /// @dev Can be executed by running `make mock`, `make local-mock`, `make deploy` or `make deploy-testnet`. 15 | contract CrossTransferOwnershipOfBaseWorldID is Script { 16 | uint256 public privateKey; 17 | 18 | address public baseStateBridgeAddress; 19 | 20 | address public newOwner; 21 | 22 | /// @notice in CrossDomainOwnable3.sol, isLocal is used to set ownership to a new address with a toggle 23 | /// for local or cross domain (using the CrossDomainMessenger to pass messages) 24 | bool public isLocal; 25 | 26 | function setUp() public { 27 | /////////////////////////////////////////////////////////////////// 28 | /// CONFIG /// 29 | /////////////////////////////////////////////////////////////////// 30 | string memory root = vm.projectRoot(); 31 | string memory path = string.concat(root, "/src/script/.deploy-config.json"); 32 | string memory json = vm.readFile(path); 33 | 34 | privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 35 | baseStateBridgeAddress = 36 | abi.decode(vm.parseJson(json, ".baseStateBridgeAddress"), (address)); 37 | newOwner = abi.decode(vm.parseJson(json, ".newBaseWorldIDOwner"), (address)); 38 | } 39 | 40 | constructor() {} 41 | 42 | function run() public { 43 | /// @notice cross domain ownership flag 44 | /// false = cross domain (address on Ethereum) 45 | /// true = local (address on Optimism) 46 | isLocal = false; 47 | 48 | vm.startBroadcast(privateKey); 49 | 50 | bytes memory call = 51 | abi.encodeCall(IOpStateBridgeTransferOwnership.transferOwnershipOp, (newOwner, isLocal)); 52 | 53 | (bool ok,) = baseStateBridgeAddress.call(call); 54 | 55 | require(ok, "call failed"); 56 | 57 | vm.stopBroadcast(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/script/initialize/op-stack/base/CrossTransferOwnershipOfBaseWorldID.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {OpWorldID} from "src/OpWorldID.sol"; 6 | import {ICrossDomainOwnable3} from "src/interfaces/ICrossDomainOwnable3.sol"; 7 | import {IOpStateBridgeTransferOwnership} from "src/interfaces/IOpStateBridgeTransferOwnership.sol"; 8 | import {OpStateBridge} from "src/OpStateBridge.sol"; 9 | 10 | /// @title Ownership Transfer of OpWorldID on Base script 11 | /// @notice forge script for transferring ownership of OpWorldID to a local (Base) 12 | /// or cross-chain (Ethereum) EOA or contract 13 | /// @author Worldcoin 14 | /// @dev Can be executed by running `make mock`, `make local-mock`, `make deploy` or `make deploy-testnet`. 15 | contract CrossTransferOwnershipOfBaseWorldID is Script { 16 | uint256 public privateKey; 17 | 18 | address public baseStateBridgeAddress; 19 | 20 | address public newOwner; 21 | 22 | /// @notice in CrossDomainOwnable3.sol, isLocal is used to set ownership to a new address with a toggle 23 | /// for local or cross domain (using the CrossDomainMessenger to pass messages) 24 | bool public isLocal; 25 | 26 | function setUp() public { 27 | /////////////////////////////////////////////////////////////////// 28 | /// CONFIG /// 29 | /////////////////////////////////////////////////////////////////// 30 | string memory root = vm.projectRoot(); 31 | string memory path = string.concat(root, "/src/script/.deploy-config.json"); 32 | string memory json = vm.readFile(path); 33 | 34 | privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 35 | baseStateBridgeAddress = 36 | abi.decode(vm.parseJson(json, ".baseStateBridgeAddress"), (address)); 37 | newOwner = abi.decode(vm.parseJson(json, ".baseStateBridgeAddress"), (address)); 38 | } 39 | 40 | constructor() {} 41 | 42 | function run() public { 43 | /// @notice cross domain ownership flag 44 | /// false = cross domain (address on Ethereum) 45 | /// true = local (address on Optimism) 46 | isLocal = false; 47 | 48 | vm.startBroadcast(privateKey); 49 | 50 | bytes memory call = 51 | abi.encodeCall(IOpStateBridgeTransferOwnership.transferOwnershipOp, (newOwner, isLocal)); 52 | 53 | (bool ok,) = baseStateBridgeAddress.call(call); 54 | 55 | require(ok, "call failed"); 56 | 57 | vm.stopBroadcast(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/script/ownership/optimism/CrossTransferOwnershipOfOptimismWorldID.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {OpWorldID} from "src/OpWorldID.sol"; 6 | import {ICrossDomainOwnable3} from "src/interfaces/ICrossDomainOwnable3.sol"; 7 | import {IOpStateBridgeTransferOwnership} from "src/interfaces/IOpStateBridgeTransferOwnership.sol"; 8 | import {OpStateBridge} from "src/OpStateBridge.sol"; 9 | 10 | /// @title Ownership Transfer of OpWorldID script on Optimism 11 | /// @notice forge script for transferring ownership of OpWorldID to a local (Optimism) 12 | /// or cross-chain (Ethereum) EOA or contract 13 | /// @author Worldcoin 14 | /// @dev Can be executed by running `make mock`, `make local-mock`, `make deploy` or `make deploy-testnet`. 15 | contract CrossTransferOwnershipOfOptimismWorldID is Script { 16 | uint256 public privateKey; 17 | 18 | address public optimismStateBridgeAddress; 19 | 20 | address public newOwner; 21 | 22 | /// @notice in CrossDomainOwnable3.sol, isLocal is used to set ownership to a new address with a toggle 23 | /// for local or cross domain (using the CrossDomainMessenger to pass messages) 24 | bool public isLocal; 25 | 26 | uint32 public opGasLimit; 27 | 28 | function setUp() public { 29 | /////////////////////////////////////////////////////////////////// 30 | /// CONFIG /// 31 | /////////////////////////////////////////////////////////////////// 32 | string memory root = vm.projectRoot(); 33 | string memory path = string.concat(root, "/src/script/.deploy-config.json"); 34 | string memory json = vm.readFile(path); 35 | 36 | privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 37 | optimismStateBridgeAddress = 38 | abi.decode(vm.parseJson(json, ".optimismStateBridgeAddress"), (address)); 39 | newOwner = abi.decode(vm.parseJson(json, ".newOptimismWorldIDOwner"), (address)); 40 | } 41 | 42 | constructor() {} 43 | 44 | function run() public { 45 | /// @notice cross domain ownership flag 46 | /// false = cross domain (address on Ethereum) 47 | /// true = local (address on Optimism) 48 | isLocal = false; 49 | 50 | vm.startBroadcast(privateKey); 51 | 52 | bytes memory call = 53 | abi.encodeCall(IOpStateBridgeTransferOwnership.transferOwnershipOp, (newOwner, isLocal)); 54 | 55 | (bool ok,) = optimismStateBridgeAddress.call(call); 56 | 57 | require(ok, "call failed"); 58 | 59 | vm.stopBroadcast(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/script/deploy/polygon/DeployPolygonStateBridgeMainnet.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {PolygonStateBridge} from "src/PolygonStateBridge.sol"; 6 | 7 | /// @title Deploy PolygonStateBridge on Mainnet 8 | /// @notice forge script to deploy PolygonStateBridge.sol 9 | /// @author Worldcoin 10 | /// @dev Can be executed by running `make mock`, `make deploy` or `make deploy-testnet`. 11 | contract DeployPolygonStateBridgeMainnet is Script { 12 | PolygonStateBridge public bridge; 13 | 14 | address public worldIDIdentityManagerAddress; 15 | address public polygonWorldIDAddress; 16 | address public checkpointManagerAddress; 17 | address public fxRootAddress; 18 | 19 | /////////////////////////////////////////////////////////////////// 20 | /// CONFIG /// 21 | /////////////////////////////////////////////////////////////////// 22 | string public root = vm.projectRoot(); 23 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 24 | string public json = vm.readFile(path); 25 | 26 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 27 | 28 | function setUp() public { 29 | /////////////////////////////////////////////////////////////////// 30 | /// POLYGON /// 31 | /////////////////////////////////////////////////////////////////// 32 | 33 | // https://static.matic.network/network/mainnet/v1/index.json 34 | // RootChainManagerProxy 35 | checkpointManagerAddress = address(0x86E4Dc95c7FBdBf52e33D563BbDB00823894C287); 36 | // FxRoot 37 | fxRootAddress = address(0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2); 38 | 39 | /////////////////////////////////////////////////////////////////// 40 | /// WORLD ID /// 41 | /////////////////////////////////////////////////////////////////// 42 | worldIDIdentityManagerAddress = 43 | abi.decode(vm.parseJson(json, ".worldIDIdentityManagerAddress"), (address)); 44 | polygonWorldIDAddress = abi.decode(vm.parseJson(json, ".polygonWorldIDAddress"), (address)); 45 | } 46 | 47 | function run() public { 48 | vm.startBroadcast(privateKey); 49 | 50 | bridge = new PolygonStateBridge( 51 | checkpointManagerAddress, fxRootAddress, worldIDIdentityManagerAddress 52 | ); 53 | 54 | bridge.setFxChildTunnel(polygonWorldIDAddress); 55 | 56 | vm.stopBroadcast(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/script/initialize/op-stack/optimism/CrossTransferOwnershipOfOptimismWorldID.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {OpWorldID} from "src/OpWorldID.sol"; 6 | import {ICrossDomainOwnable3} from "src/interfaces/ICrossDomainOwnable3.sol"; 7 | import {IOpStateBridgeTransferOwnership} from "src/interfaces/IOpStateBridgeTransferOwnership.sol"; 8 | import {OpStateBridge} from "src/OpStateBridge.sol"; 9 | 10 | /// @title Ownership Transfer of OpWorldID script on Optimism 11 | /// @notice forge script for transferring ownership of OpWorldID to a local (Optimism) 12 | /// or cross-chain (Ethereum) EOA or contract 13 | /// @author Worldcoin 14 | /// @dev Can be executed by running `make mock`, `make local-mock`, `make deploy` or `make deploy-testnet`. 15 | contract CrossTransferOwnershipOfOptimismWorldID is Script { 16 | uint256 public privateKey; 17 | 18 | address public optimismStateBridgeAddress; 19 | 20 | address public newOwner; 21 | 22 | /// @notice in CrossDomainOwnable3.sol, isLocal is used to set ownership to a new address with a toggle 23 | /// for local or cross domain (using the CrossDomainMessenger to pass messages) 24 | bool public isLocal; 25 | 26 | uint32 public opGasLimit; 27 | 28 | function setUp() public { 29 | /////////////////////////////////////////////////////////////////// 30 | /// CONFIG /// 31 | /////////////////////////////////////////////////////////////////// 32 | string memory root = vm.projectRoot(); 33 | string memory path = string.concat(root, "/src/script/.deploy-config.json"); 34 | string memory json = vm.readFile(path); 35 | 36 | privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 37 | optimismStateBridgeAddress = 38 | abi.decode(vm.parseJson(json, ".optimismStateBridgeAddress"), (address)); 39 | newOwner = abi.decode(vm.parseJson(json, ".optimismStateBridgeAddress"), (address)); 40 | } 41 | 42 | constructor() {} 43 | 44 | function run() public { 45 | /// @notice cross domain ownership flag 46 | /// false = cross domain (address on Ethereum) 47 | /// true = local (address on Optimism) 48 | isLocal = false; 49 | 50 | vm.startBroadcast(privateKey); 51 | 52 | bytes memory call = 53 | abi.encodeCall(IOpStateBridgeTransferOwnership.transferOwnershipOp, (newOwner, isLocal)); 54 | 55 | (bool ok,) = optimismStateBridgeAddress.call(call); 56 | 57 | require(ok, "call failed"); 58 | 59 | vm.stopBroadcast(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/script/deploy/polygon/DeployPolygonStateBridgeGoerli.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {PolygonStateBridge} from "src/PolygonStateBridge.sol"; 6 | 7 | /// @title PolygonState Bridge deployment script 8 | /// @notice forge script to deploy StateBridge.sol 9 | /// @author Worldcoin 10 | /// @dev Can be executed by running `make mock`, `make local-mock`, `make deploy` or `make deploy-testnet`. 11 | contract DeployPolygonStateBridgeGoerli is Script { 12 | PolygonStateBridge public bridge; 13 | 14 | address public opWorldIDAddress; 15 | address public polygonWorldIDAddress; 16 | address public worldIDIdentityManagerAddress; 17 | 18 | address public checkpointManagerAddress; 19 | address public fxRootAddress; 20 | 21 | /////////////////////////////////////////////////////////////////// 22 | /// CONFIG /// 23 | /////////////////////////////////////////////////////////////////// 24 | string public root = vm.projectRoot(); 25 | string public path = string.concat(root, "/src/script/.deploy-config.json"); 26 | string public json = vm.readFile(path); 27 | 28 | uint256 public privateKey = abi.decode(vm.parseJson(json, ".privateKey"), (uint256)); 29 | 30 | function setUp() public { 31 | /////////////////////////////////////////////////////////////////// 32 | /// POLYGON /// 33 | /////////////////////////////////////////////////////////////////// 34 | 35 | // https://static.matic.network/network/testnet/mumbai/index.json 36 | // RoootChainManagerProxy 37 | checkpointManagerAddress = address(0x2890bA17EfE978480615e330ecB65333b880928e); 38 | 39 | // FxRoot 40 | fxRootAddress = address(0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA); 41 | 42 | /////////////////////////////////////////////////////////////////// 43 | /// WORLD ID /// 44 | /////////////////////////////////////////////////////////////////// 45 | worldIDIdentityManagerAddress = 46 | abi.decode(vm.parseJson(json, ".worldIDIdentityManagerAddress"), (address)); 47 | polygonWorldIDAddress = abi.decode(vm.parseJson(json, ".polygonWorldIDAddress"), (address)); 48 | } 49 | 50 | function run() public { 51 | vm.startBroadcast(privateKey); 52 | 53 | bridge = new PolygonStateBridge( 54 | checkpointManagerAddress, fxRootAddress, worldIDIdentityManagerAddress 55 | ); 56 | 57 | bridge.setFxChildTunnel(polygonWorldIDAddress); 58 | 59 | vm.stopBroadcast(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/mock/MockBridgedWorldID.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {WorldIDBridge} from "src/abstract/WorldIDBridge.sol"; 5 | import {SemaphoreTreeDepthValidator} from "src/utils/SemaphoreTreeDepthValidator.sol"; 6 | import {SemaphoreVerifier} from "src/SemaphoreVerifier.sol"; 7 | import {Ownable} from "openzeppelin-contracts/access/Ownable.sol"; 8 | 9 | /// @title OPWorldID and PolygonWorldID Mock 10 | /// @author Worldcoin 11 | /// @notice Mock of PolygonWorldID and OpWorldID in order to test functionality on a local chain 12 | /// @custom:deployment deployed through make local-mock 13 | contract MockBridgedWorldID is WorldIDBridge, Ownable { 14 | /////////////////////////////////////////////////////////////////////////////// 15 | /// CONSTRUCTION /// 16 | /////////////////////////////////////////////////////////////////////////////// 17 | 18 | /// @notice Initializes the contract the depth of the associated merkle tree. 19 | /// 20 | /// @param _treeDepth The depth of the WorldID Semaphore merkle tree. 21 | constructor(uint8 _treeDepth) WorldIDBridge(_treeDepth) {} 22 | 23 | /////////////////////////////////////////////////////////////////////////////// 24 | /// ROOT MIRRORING /// 25 | /////////////////////////////////////////////////////////////////////////////// 26 | 27 | /// @notice This function is called by the state bridge contract when it forwards a new root to 28 | /// the bridged WorldID. 29 | /// @dev This function can revert if Optimism's CrossDomainMessenger stops processing proofs 30 | /// or if OPLabs stops submitting them. Next iteration of Optimism's cross-domain messaging, will be 31 | /// fully permissionless for message-passing, so this will not be an issue. 32 | /// Sequencer needs to include changes to the CrossDomainMessenger contract on L1, 33 | /// not economically penalized if messages are not included, however the fraud prover (Cannon) 34 | /// can force the sequencer to include it. 35 | /// 36 | /// @param newRoot The value of the new root. 37 | /// 38 | /// @custom:reverts CannotOverwriteRoot If the root already exists in the root history. 39 | /// @custom:reverts string If the caller is not the owner. 40 | function receiveRoot(uint256 newRoot) public virtual onlyOwner { 41 | _receiveRoot(newRoot); 42 | } 43 | 44 | /////////////////////////////////////////////////////////////////////////////// 45 | /// DATA MANAGEMENT /// 46 | /////////////////////////////////////////////////////////////////////////////// 47 | 48 | /// @notice Sets the amount of time it takes for a root in the root history to expire. 49 | /// 50 | /// @param expiryTime The new amount of time it takes for a root to expire. 51 | /// 52 | /// @custom:reverts string If the caller is not the owner. 53 | function setRootHistoryExpiry(uint256 expiryTime) public virtual override onlyOwner { 54 | _setRootHistoryExpiry(expiryTime); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/OpWorldID.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {WorldIDBridge} from "./abstract/WorldIDBridge.sol"; 5 | 6 | import {IOpWorldID} from "./interfaces/IOpWorldID.sol"; 7 | import {CrossDomainOwnable3} from 8 | "@eth-optimism/contracts-bedrock/contracts/L2/CrossDomainOwnable3.sol"; 9 | 10 | /// @title Optimism World ID Bridge 11 | /// @author Worldcoin 12 | /// @notice A contract that manages the root history of the Semaphore identity merkle tree on 13 | /// Optimism. 14 | /// @dev This contract is deployed on Optimism and is called by the L1 Proxy contract for each new 15 | /// root insertion. 16 | contract OpWorldID is WorldIDBridge, CrossDomainOwnable3, IOpWorldID { 17 | /////////////////////////////////////////////////////////////////////////////// 18 | /// CONSTRUCTION /// 19 | /////////////////////////////////////////////////////////////////////////////// 20 | 21 | /// @notice Initializes the contract the depth of the associated merkle tree. 22 | /// 23 | /// @param _treeDepth The depth of the WorldID Semaphore merkle tree. 24 | constructor(uint8 _treeDepth) WorldIDBridge(_treeDepth) {} 25 | 26 | /////////////////////////////////////////////////////////////////////////////// 27 | /// ROOT MIRRORING /// 28 | /////////////////////////////////////////////////////////////////////////////// 29 | 30 | /// @notice This function is called by the state bridge contract when it forwards a new root to 31 | /// the bridged WorldID. 32 | /// @dev This function can revert if Optimism's CrossDomainMessenger stops processing proofs 33 | /// or if OPLabs stops submitting them. Next iteration of Optimism's cross-domain messaging, will be 34 | /// fully permissionless for message-passing, so this will not be an issue. 35 | /// Sequencer needs to include changes to the CrossDomainMessenger contract on L1, 36 | /// not economically penalized if messages are not included, however the fraud prover (Cannon) 37 | /// can force the sequencer to include it. 38 | /// 39 | /// @param newRoot The value of the new root. 40 | /// 41 | /// @custom:reverts CannotOverwriteRoot If the root already exists in the root history. 42 | /// @custom:reverts string If the caller is not the owner. 43 | function receiveRoot(uint256 newRoot) external virtual onlyOwner { 44 | _receiveRoot(newRoot); 45 | } 46 | 47 | /////////////////////////////////////////////////////////////////////////////// 48 | /// DATA MANAGEMENT /// 49 | /////////////////////////////////////////////////////////////////////////////// 50 | 51 | /// @notice Sets the amount of time it takes for a root in the root history to expire. 52 | /// 53 | /// @param expiryTime The new amount of time it takes for a root to expire. 54 | /// 55 | /// @custom:reverts string If the caller is not the owner. 56 | function setRootHistoryExpiry(uint256 expiryTime) public virtual override onlyOwner { 57 | _setRootHistoryExpiry(expiryTime); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/mock/MockPolygonBridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {WorldIDBridge} from "../abstract/WorldIDBridge.sol"; 5 | import {IWorldIDIdentityManager} from "../interfaces/IWorldIDIdentityManager.sol"; 6 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 7 | import {BytesUtils} from "src/utils/BytesUtils.sol"; 8 | 9 | /// @title Polygon WorldID Bridge Mock 10 | /// @author Worldcoin 11 | /// @dev of the Polygon FxPortal Bridge to test low-level assembly functions 12 | /// `grabSelector` and `stripSelector` in the PolygonWorldID contract 13 | contract MockPolygonBridge is WorldIDBridge, Ownable { 14 | /// @notice The selector of the `receiveRoot` function. 15 | /// @dev this selector is precomputed in the constructor to not have to recompute them for every 16 | /// call of the _processMesageFromRoot function 17 | bytes4 internal _receiveRootSelector; 18 | 19 | /// @notice The selector of the `receiveRootHistoryExpiry` function. 20 | /// @dev this selector is precomputed in the constructor to not have to recompute them for every 21 | /// call of the _processMesageFromRoot function 22 | bytes4 internal _receiveRootHistoryExpirySelector; 23 | 24 | /// @notice Emitted when the message selector passed from FxRoot is invalid. 25 | error InvalidMessageSelector(bytes4 selector); 26 | 27 | /// @notice Initializes the contract's storage variables with the correct parameters 28 | /// 29 | /// @param _treeDepth The depth of the WorldID Identity Manager merkle tree. 30 | constructor(uint8 _treeDepth) WorldIDBridge(_treeDepth) { 31 | _receiveRootSelector = bytes4(keccak256("receiveRoot(uint256)")); 32 | _receiveRootHistoryExpirySelector = bytes4(keccak256("setRootHistoryExpiry(uint256)")); 33 | } 34 | 35 | /////////////////////////////////////////////////////////////////////////////// 36 | /// ROOT MIRRORING /// 37 | /////////////////////////////////////////////////////////////////////////////// 38 | 39 | /// @notice Mock for Polygon's FxPortal bridge functionality 40 | /// 41 | /// @param message An ABI-encoded tuple of `(uint256 newRoot, uint128 supersedeTimestamp)` that 42 | /// is used to call `receiveRoot`. 43 | function processMessageFromRoot(bytes memory message) public onlyOwner { 44 | // I need to decode selector and payload here 45 | bytes4 selector = bytes4(BytesUtils.substring(message, 0, 4)); 46 | bytes memory payload = BytesUtils.substring(message, 4, message.length - 4); 47 | 48 | if (selector == _receiveRootSelector) { 49 | uint256 root = abi.decode(payload, (uint256)); 50 | _receiveRoot(root); 51 | } else if (selector == _receiveRootHistoryExpirySelector) { 52 | uint256 newRootHistoryExpiry = abi.decode(payload, (uint256)); 53 | _setRootHistoryExpiry(newRootHistoryExpiry); 54 | } else { 55 | revert InvalidMessageSelector(selector); 56 | } 57 | } 58 | 59 | /////////////////////////////////////////////////////////////////////////////// 60 | /// DATA MANAGEMENT /// 61 | /////////////////////////////////////////////////////////////////////////////// 62 | 63 | /// @notice Placeholder to satisfy WorldIDBridge inheritance 64 | /// @dev This function is not used on Polygon PoS because of FxPortal message passing architecture 65 | function setRootHistoryExpiry(uint256) public virtual override { 66 | revert("PolygonWorldID: Root history expiry should only be set via the state bridge"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | # Full reference https://github.com/foundry-rs/foundry/tree/master/config 2 | 3 | # === Default Profile ========================================================= 4 | 5 | [profile.default] 6 | 7 | solc = "0.8.15" 8 | optimizer = true 9 | optimizer_runs = 10_000 10 | fuzz = {runs = 256} 11 | fs_permissions = [{ access = "read", path = "./"}] 12 | 13 | src="src" 14 | libs = ["lib"] 15 | out = "out" 16 | test = "test" 17 | 18 | gas_reports = ["*"] 19 | 20 | auto_detect_solc = false 21 | bytecode_hash = "none" 22 | 23 | remappings = [ 24 | "@prb/test/=lib/prb-test/src/", 25 | "forge-std/=lib/forge-std/src/", 26 | "src/=src/", 27 | "solmate/=lib/solmate/", 28 | "@rari-capital/solmate/=lib/solmate/", 29 | "@eth-optimism/contracts/=node_modules/@eth-optimism/contracts/", 30 | "@eth-optimism/contracts-bedrock/=node_modules/@eth-optimism/contracts-bedrock/", 31 | "openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/", 32 | "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", 33 | "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", 34 | ] 35 | 36 | # Make formatting consistent. 37 | [profile.default.fmt] 38 | line_length = 100 # The maximum length of lines. 39 | tab_width = 4 # Indent by two spaces for each level. 40 | bracket_spacing = false # Don't put spaces between brackets and content. 41 | int_types = "long" # Specify full integer type names. 42 | 43 | # We want to optimise via the new optimiser backend for better results. 44 | via_ir = true 45 | 46 | # We can have quite some control over the optimiser when using the new IR one. 47 | [profile.default.optimizer_details] 48 | peephole = true # Enables the peephole optimisation. 49 | inliner = true # Enables the inliner. 50 | jumpdest_remover = true # Enables the elimination of jump destinations. 51 | order_literals = true # Allows re-ordering literals in commutative ops. 52 | deduplicate = true # Removes duplicate code blocks. 53 | cse = true # Enables common subexpression elimination. Useful. 54 | constant_optimizer = true # Computes some constant expressions at compile time. 55 | yul = true # Enables the new ABI optimiser. 56 | 57 | [profile.default.optimizer_details.yul_details] 58 | stack_allocation = true # Improves allocation of stack slots for variables. 59 | 60 | # === Debug Profile =========================================================== 61 | 62 | [profile.debug] 63 | # Make things chattier when debugging in case of test failures, giving us more 64 | # information with which to debug the issue. At this level, stack traces and 65 | # setup traces for failing tests are displayed. 66 | verbosity = 3 67 | 68 | [profile.ci] 69 | fuzz = {runs = 10_000} 70 | verbosity = 4 71 | 72 | 73 | # === Test All Profile ======================================================== 74 | 75 | [profile.bench] 76 | # We make the optimiser run a lot more for production, hoping to reduce our gas 77 | # costs. 78 | optimizer_runs = 20000 79 | 80 | # Make things chattier when debugging in case of test failures, giving us more 81 | # information with which to debug the issue. At this level, stack traces for 82 | # failing tests are displayed. 83 | verbosity = 3 84 | 85 | # We can specify the contracts to track gas data for by tracing. 86 | gas_reports = [ 87 | "WorldIDIdentityManager", 88 | "MockWorldIDIdentityManager", 89 | "Verifier", 90 | ] 91 | 92 | # === Production Profile ====================================================== 93 | 94 | [profile.production] 95 | # We make the optimiser run a lot more for production, hoping to reduce our gas 96 | # costs. 97 | fs_permissions = [{ access = "read", path = "./"}] 98 | optimizer_runs = 20000 99 | -------------------------------------------------------------------------------- /src/test/BytesUtils.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.15; 2 | 3 | import {BytesUtils} from "src/utils/BytesUtils.sol"; 4 | import {PRBTest} from "@prb/test/PRBTest.sol"; 5 | import {StdCheats} from "forge-std/StdCheats.sol"; 6 | 7 | /// @title BytesUtils Test 8 | /// @author Worldcoin 9 | /// @notice Tests the low-level assembly functions `grabSelector` and `stripSelector` in the BytesUtils library 10 | contract BytesUtilsTest is PRBTest, StdCheats { 11 | /// @notice Emitted when the payload is too short to contain a selector (at least 4 bytes). 12 | error PayloadTooShort(); 13 | 14 | /////////////////////////////////////////////////////////////////// 15 | /// SUCCEEDS /// 16 | /////////////////////////////////////////////////////////////////// 17 | 18 | /// @notice Tests that the `grabSelector` function returns the first 4 bytes of a payload. 19 | function testGrabSelectorSucceeds(string memory sig) public { 20 | bytes4 selector = bytes4(keccak256(abi.encodePacked(sig))); 21 | 22 | bytes memory encodedMessage = abi.encodeWithSignature(sig, 4844, 4337); 23 | 24 | bytes4 grabbedSelector = BytesUtils.grabSelector(encodedMessage); 25 | 26 | assertEq(selector, grabbedSelector); 27 | } 28 | 29 | /// @notice Tests that the `stripSelector` function returns the payload after the first 4 bytes. 30 | function testStripSelectorSucceeds(string memory sig) public { 31 | bytes memory encodedMessage = abi.encodeWithSignature(sig, 4844, 4337); 32 | 33 | bytes memory strippedPayload = BytesUtils.stripSelector(encodedMessage); 34 | 35 | bytes memory expectedPayload = abi.encodePacked(uint256(4844), uint256(4337)); 36 | 37 | assertEq(strippedPayload, expectedPayload); 38 | } 39 | 40 | /// @notice tests that different function signatures create different selectors (a bit obvious) 41 | function testDifferentSigsDontCollideSucceeds(string memory sig, string memory notSig) public { 42 | vm.assume(keccak256(bytes(sig)) != keccak256(bytes(notSig))); 43 | 44 | bytes4 selector = bytes4(keccak256(abi.encodePacked(sig))); 45 | 46 | bytes memory encodedMessage = abi.encodeWithSignature(notSig, 4844, 4337); 47 | 48 | bytes4 grabbedSelector = BytesUtils.grabSelector(encodedMessage); 49 | 50 | assertTrue(selector != grabbedSelector); 51 | } 52 | 53 | /////////////////////////////////////////////////////////////////// 54 | /// REVERTS /// 55 | /////////////////////////////////////////////////////////////////// 56 | 57 | /// @notice Tests that the `grabSelector` function reverts when the payload is too short (<4 bytes) 58 | /// to contain a selector. 59 | function testGrabSelectorPayloadTooShortReverts( 60 | bytes2 lessThanFourBytes, 61 | bytes4 fourOrMoreBytes 62 | ) public { 63 | // works fine 64 | BytesUtils.grabSelector(abi.encodePacked(fourOrMoreBytes)); 65 | 66 | vm.expectRevert(PayloadTooShort.selector); 67 | 68 | // reverts 69 | BytesUtils.grabSelector(abi.encodePacked(lessThanFourBytes)); 70 | } 71 | 72 | /// @notice Tests that the `stripSelector` function reverts when the payload is too short (<5 bytes) 73 | /// to contain a payload after the selctor 74 | function testStripSelectorPayloadTooShortReverts( 75 | bytes4 fourBytesOrLess, 76 | bytes5 moreThanFourBytes 77 | ) public { 78 | // works fine 79 | BytesUtils.stripSelector(abi.encodePacked(moreThanFourBytes)); 80 | 81 | vm.expectRevert(PayloadTooShort.selector); 82 | 83 | // reverts 84 | BytesUtils.stripSelector((abi.encodePacked(fourBytesOrLess))); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/data/semaphore_16.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "groth16", 3 | "curve": "bn128", 4 | "nPublic": 4, 5 | "vk_alpha_1": [ 6 | "20491192805390485299153009773594534940189261866228447918068658471970481763042", 7 | "9383485363053290200918347156157836566562967994039712273449902621266178545958", 8 | "1" 9 | ], 10 | "vk_beta_2": [ 11 | [ 12 | "6375614351688725206403948262868962793625744043794305715222011528459656738731", 13 | "4252822878758300859123897981450591353533073413197771768651442665752259397132" 14 | ], 15 | [ 16 | "10505242626370262277552901082094356697409835680220590971873171140371331206856", 17 | "21847035105528745403288232691147584728191162732299865338377159692350059136679" 18 | ], 19 | ["1", "0"] 20 | ], 21 | "vk_gamma_2": [ 22 | [ 23 | "10857046999023057135944570762232829481370756359578518086990519993285655852781", 24 | "11559732032986387107991004021392285783925812861821192530917403151452391805634" 25 | ], 26 | [ 27 | "8495653923123431417604973247489272438418190587263600148770280649306958101930", 28 | "4082367875863433681332203403145435568316851327593401208105741076214120093531" 29 | ], 30 | ["1", "0"] 31 | ], 32 | "vk_delta_2": [ 33 | [ 34 | "16243966861079634958125511652590761846958471358623040426599000904006426210032", 35 | "13406811599156507528361773763681356312643537981039994686313383243831956396116" 36 | ], 37 | [ 38 | "15688083679237922164673518758181461582601853873216319711156397437601833996222", 39 | "11781596534582143578120404722739278517564025497573071755253972265891888117374" 40 | ], 41 | ["1", "0"] 42 | ], 43 | "vk_alphabeta_12": [ 44 | [ 45 | [ 46 | "2029413683389138792403550203267699914886160938906632433982220835551125967885", 47 | "21072700047562757817161031222997517981543347628379360635925549008442030252106" 48 | ], 49 | [ 50 | "5940354580057074848093997050200682056184807770593307860589430076672439820312", 51 | "12156638873931618554171829126792193045421052652279363021382169897324752428276" 52 | ], 53 | [ 54 | "7898200236362823042373859371574133993780991612861777490112507062703164551277", 55 | "7074218545237549455313236346927434013100842096812539264420499035217050630853" 56 | ] 57 | ], 58 | [ 59 | [ 60 | "7077479683546002997211712695946002074877511277312570035766170199895071832130", 61 | "10093483419865920389913245021038182291233451549023025229112148274109565435465" 62 | ], 63 | [ 64 | "4595479056700221319381530156280926371456704509942304414423590385166031118820", 65 | "19831328484489333784475432780421641293929726139240675179672856274388269393268" 66 | ], 67 | [ 68 | "11934129596455521040620786944827826205713621633706285934057045369193958244500", 69 | "8037395052364110730298837004334506829870972346962140206007064471173334027475" 70 | ] 71 | ] 72 | ], 73 | "IC": [ 74 | [ 75 | "1964404930528116823793003656764176108669615750422202377358993070935069307720", 76 | "2137714996673694828207437580381836490878070731768805974506391024595988817424", 77 | "1" 78 | ], 79 | [ 80 | "19568893707760843340848992184233194433177372925415116053368211122719346671126", 81 | "11639469568629189918046964192305250472192697612201524135560178632824282818614", 82 | "1" 83 | ], 84 | [ 85 | "5317268879687484957437879782519918549127939892210247573193613900261494313825", 86 | "528174394975085006443543773707702838726735933116136102590448357278717993744", 87 | "1" 88 | ], 89 | [ 90 | "14865918005176722116473730206622066845866539143554731094374354951675249722731", 91 | "3197770568483953664363740385883457803041685902965668289308665954510373380344", 92 | "1" 93 | ], 94 | [ 95 | "6863358721495494421022713667808247652425178970453300712435830652679038918987", 96 | "15025816433373311798308762709072064417001390853103872064614174594927359131281", 97 | "1" 98 | ] 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /src/test/data/semaphore_30.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "groth16", 3 | "curve": "bn128", 4 | "nPublic": 4, 5 | "vk_alpha_1": [ 6 | "20491192805390485299153009773594534940189261866228447918068658471970481763042", 7 | "9383485363053290200918347156157836566562967994039712273449902621266178545958", 8 | "1" 9 | ], 10 | "vk_beta_2": [ 11 | [ 12 | "6375614351688725206403948262868962793625744043794305715222011528459656738731", 13 | "4252822878758300859123897981450591353533073413197771768651442665752259397132" 14 | ], 15 | [ 16 | "10505242626370262277552901082094356697409835680220590971873171140371331206856", 17 | "21847035105528745403288232691147584728191162732299865338377159692350059136679" 18 | ], 19 | ["1", "0"] 20 | ], 21 | "vk_gamma_2": [ 22 | [ 23 | "10857046999023057135944570762232829481370756359578518086990519993285655852781", 24 | "11559732032986387107991004021392285783925812861821192530917403151452391805634" 25 | ], 26 | [ 27 | "8495653923123431417604973247489272438418190587263600148770280649306958101930", 28 | "4082367875863433681332203403145435568316851327593401208105741076214120093531" 29 | ], 30 | ["1", "0"] 31 | ], 32 | "vk_delta_2": [ 33 | [ 34 | "15028154694713144242204861571552635520290993855826554325002991692907421516918", 35 | "10202326166286888893675634318107715186834588694714750762952081034135561546271" 36 | ], 37 | [ 38 | "12766289885372833812620582632847872978085960777075662988932200910695848591357", 39 | "18486039841380105976272577521609866666900576498507352937328726490052296469859" 40 | ], 41 | ["1", "0"] 42 | ], 43 | "vk_alphabeta_12": [ 44 | [ 45 | [ 46 | "2029413683389138792403550203267699914886160938906632433982220835551125967885", 47 | "21072700047562757817161031222997517981543347628379360635925549008442030252106" 48 | ], 49 | [ 50 | "5940354580057074848093997050200682056184807770593307860589430076672439820312", 51 | "12156638873931618554171829126792193045421052652279363021382169897324752428276" 52 | ], 53 | [ 54 | "7898200236362823042373859371574133993780991612861777490112507062703164551277", 55 | "7074218545237549455313236346927434013100842096812539264420499035217050630853" 56 | ] 57 | ], 58 | [ 59 | [ 60 | "7077479683546002997211712695946002074877511277312570035766170199895071832130", 61 | "10093483419865920389913245021038182291233451549023025229112148274109565435465" 62 | ], 63 | [ 64 | "4595479056700221319381530156280926371456704509942304414423590385166031118820", 65 | "19831328484489333784475432780421641293929726139240675179672856274388269393268" 66 | ], 67 | [ 68 | "11934129596455521040620786944827826205713621633706285934057045369193958244500", 69 | "8037395052364110730298837004334506829870972346962140206007064471173334027475" 70 | ] 71 | ] 72 | ], 73 | "IC": [ 74 | [ 75 | "1452272927738590248356371174422184656932731110936062990115610832462181634644", 76 | "3608050114233210789542189629343107890943266759827387991788718454179833288695", 77 | "1" 78 | ], 79 | [ 80 | "14798240452388909327945424685903532333765637883272751382037716636327236955001", 81 | "10773894897711848209682368488916121016695006898681985691467605219098835500201", 82 | "1" 83 | ], 84 | [ 85 | "17204267933132009093604099819536245144503489322639121825381131096467570698650", 86 | "7704298975420304156332734115679983371345754866278811368869074990486717531131", 87 | "1" 88 | ], 89 | [ 90 | "8060465662017324080560848316478407038163145149983639907596180500095598669247", 91 | "20475082166427284188002500222093571716651248980245637602667562336751029856573", 92 | "1" 93 | ], 94 | [ 95 | "7457566682692308112726332096733260585025339741083447785327706250123165087868", 96 | "11904519443874922292602150685069370036383697877657723976244907400392778002614", 97 | "1" 98 | ] 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /src/test/MockPolygonBridge.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.15; 2 | 3 | import {MockPolygonBridge} from "src/mock/MockPolygonBridge.sol"; 4 | import {WorldIDBridge} from "src/abstract/WorldIDBridge.sol"; 5 | import {PRBTest} from "@prb/test/PRBTest.sol"; 6 | import {StdCheats} from "forge-std/StdCheats.sol"; 7 | 8 | /// @title Mock Polygon Bridge Test 9 | /// @author Worldcoin 10 | /// @notice Mock of the Polygon FxPortal Bridge to test low-level assembly functions 11 | /// `grabSelector` and `stripSelector` in the PolygonWorldID contract 12 | contract MockPolygonBridgeTest is PRBTest, StdCheats { 13 | MockPolygonBridge polygonWorldID; 14 | 15 | address owner; 16 | 17 | /// @notice The time in the `rootHistory` mapping associated with a root that has never been 18 | /// seen before. 19 | uint128 internal constant NULL_ROOT_TIME = 0; 20 | 21 | /// @notice Emitted when root history expiry is set 22 | event RootHistoryExpirySet(uint256 rootHistoryExpiry); 23 | 24 | /// @notice Emitted when a new root is received by the contract. 25 | /// 26 | /// @param root The value of the root that was added. 27 | /// @param timestamp The timestamp of insertion for the given root. 28 | event RootAdded(uint256 root, uint128 timestamp); 29 | 30 | function setUp() public { 31 | owner = address(0x1234); 32 | 33 | vm.label(owner, "owner"); 34 | 35 | vm.prank(owner); 36 | polygonWorldID = new MockPolygonBridge(uint8(30)); 37 | } 38 | 39 | /////////////////////////////////////////////////////////////////// 40 | /// SUCCEEDS /// 41 | /////////////////////////////////////////////////////////////////// 42 | 43 | /// @notice tests that receiveRoot succeeds if encoded properly 44 | function test_ReceiveRoot_succeeds(uint256 newRoot) public { 45 | bytes memory message = abi.encodeWithSignature("receiveRoot(uint256)", newRoot); 46 | 47 | vm.expectEmit(true, true, true, true); 48 | 49 | emit RootAdded(newRoot, uint128(block.timestamp)); 50 | 51 | vm.prank(owner); 52 | polygonWorldID.processMessageFromRoot(message); 53 | } 54 | 55 | /// @notice tests that setRootHistoryExpiry succeeds if encoded properly 56 | function test_setRootHistoryExpiry_succeeds(uint256 rootHistoryExpiry) public { 57 | bytes memory message = 58 | abi.encodeWithSignature("setRootHistoryExpiry(uint256)", rootHistoryExpiry); 59 | 60 | vm.expectEmit(true, true, true, true); 61 | 62 | emit RootHistoryExpirySet(rootHistoryExpiry); 63 | 64 | vm.prank(owner); 65 | polygonWorldID.processMessageFromRoot(message); 66 | } 67 | 68 | /////////////////////////////////////////////////////////////////// 69 | /// REVERTS /// 70 | /////////////////////////////////////////////////////////////////// 71 | 72 | /// @notice tests that an invalid function signature reverts 73 | function test_processMessageFromRoot_reverts_InvalidMessageSelector( 74 | bytes4 invalidSelector, 75 | bytes32 param 76 | ) public { 77 | vm.assume( 78 | invalidSelector != bytes4(keccak256("receiveRoot(uint256)")) 79 | && invalidSelector != bytes4(keccak256("setRootHistoryExpiry(uint256)")) 80 | ); 81 | 82 | bytes memory message = abi.encode(invalidSelector, param); 83 | 84 | vm.expectRevert( 85 | abi.encodeWithSelector( 86 | MockPolygonBridge.InvalidMessageSelector.selector, invalidSelector 87 | ) 88 | ); 89 | vm.prank(owner); 90 | polygonWorldID.processMessageFromRoot(message); 91 | } 92 | 93 | function test_processMessageFromRoot_reverts_CannotOverwriteRoot(uint256 newRoot) public { 94 | vm.assume(newRoot != 0); 95 | 96 | bytes memory message = abi.encodeWithSignature("receiveRoot(uint256)", newRoot); 97 | 98 | vm.expectEmit(true, true, true, true); 99 | emit RootAdded(newRoot, uint128(block.timestamp)); 100 | 101 | vm.prank(owner); 102 | polygonWorldID.processMessageFromRoot(message); 103 | 104 | vm.expectRevert(abi.encodeWithSelector(WorldIDBridge.CannotOverwriteRoot.selector)); 105 | 106 | vm.prank(owner); 107 | polygonWorldID.processMessageFromRoot(message); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/mock/MockWorldIDIdentityManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {IWorldIDIdentityManager} from "src/interfaces/IWorldIDIdentityManager.sol"; 5 | 6 | /// @title WorldID Identity Manager Mock 7 | /// @author Worldcoin 8 | /// @notice Mock of the WorldID Identity Manager contract (world-id-contracts) to test functionality on a local chain 9 | /// @dev deployed through make mock and make local-mock 10 | contract MockWorldIDIdentityManager is IWorldIDIdentityManager { 11 | uint256 internal _latestRoot; 12 | 13 | /// @notice Represents the kind of change that is made to the root of the tree. 14 | enum TreeChange { 15 | Insertion, 16 | Deletion 17 | } 18 | 19 | /// @notice Emitted when the current root of the tree is updated. 20 | /// 21 | /// @param preRoot The value of the tree's root before the update. 22 | /// @param kind Either "insertion" or "update", the kind of alteration that was made to the 23 | /// tree. 24 | /// @param postRoot The value of the tree's root after the update. 25 | event TreeChanged(uint256 indexed preRoot, TreeChange indexed kind, uint256 indexed postRoot); 26 | 27 | constructor(uint256 initRoot) { 28 | _latestRoot = initRoot; 29 | } 30 | 31 | /// @notice Registers identities into the WorldID system. 32 | /// @dev Can only be called by the identity operator. 33 | /// @dev Registration is performed off-chain and verified on-chain via the `insertionProof`. 34 | /// This saves gas and time over inserting identities one at a time. 35 | /// 36 | /// @param insertionProof The proof that given the conditions (`preRoot`, `startIndex` and 37 | /// `identityCommitments`), insertion into the tree results in `postRoot`. Elements 0 and 38 | /// 1 are the `x` and `y` coordinates for `ar` respectively. Elements 2 and 3 are the `x` 39 | /// coordinate for `bs`, and elements 4 and 5 are the `y` coordinate for `bs`. Elements 6 40 | /// and 7 are the `x` and `y` coordinates for `krs`. 41 | /// @param preRoot The value for the root of the tree before the `identityCommitments` have been 42 | //// inserted. Must be an element of the field `Kr`. (already in reduced form) 43 | /// @param startIndex The position in the tree at which the insertions were made. 44 | /// @param identityCommitments The identities that were inserted into the tree starting at 45 | /// `startIndex` and `preRoot` to give `postRoot`. All of the commitments must be 46 | /// elements of the field `Kr`. 47 | /// @param postRoot The root obtained after inserting all of `identityCommitments` into the tree 48 | /// described by `preRoot`. Must be an element of the field `Kr`. (alread in reduced form) 49 | /// 50 | function registerIdentities( 51 | uint256[8] calldata insertionProof, 52 | uint256 preRoot, 53 | uint32 startIndex, 54 | uint256[] calldata identityCommitments, 55 | uint256 postRoot 56 | ) public { 57 | _latestRoot = postRoot; 58 | emit TreeChanged(preRoot, TreeChange.Insertion, postRoot); 59 | } 60 | 61 | /// @notice Deletes identities from the WorldID system. 62 | /// @dev Can only be called by the identity operator. 63 | /// @dev Deletion is performed off-chain and verified on-chain via the `deletionProof`. 64 | /// This saves gas and time over deleting identities one at a time. 65 | /// 66 | /// @param deletionProof The proof that given the conditions (`preRoot` and `packedDeletionIndices`), 67 | /// deletion into the tree results in `postRoot`. Elements 0 and 1 are the `x` and `y` 68 | /// coordinates for `ar` respectively. Elements 2 and 3 are the `x` coordinate for `bs`, 69 | /// and elements 4 and 5 are the `y` coordinate for `bs`. Elements 6 and 7 are the `x` 70 | /// and `y` coordinates for `krs`. 71 | /// @param packedDeletionIndices The indices of the identities that were deleted from the tree. The batch size is inferred from the length of this 72 | //// array: batchSize = packedDeletionIndices / 4 73 | /// @param preRoot The value for the root of the tree before the corresponding identity commitments have 74 | /// been deleted. Must be an element of the field `Kr`. 75 | /// @param postRoot The root obtained after deleting all of `identityCommitments` into the tree 76 | /// described by `preRoot`. Must be an element of the field `Kr`. 77 | function deleteIdentities( 78 | uint256[8] calldata deletionProof, 79 | bytes calldata packedDeletionIndices, 80 | uint256 preRoot, 81 | uint256 postRoot 82 | ) public { 83 | _latestRoot = postRoot; 84 | emit TreeChanged(preRoot, TreeChange.Deletion, postRoot); 85 | } 86 | 87 | function insertRoot(uint256 postRoot) public { 88 | uint256 preRoot = _latestRoot; 89 | _latestRoot = postRoot; 90 | 91 | emit TreeChanged(preRoot, TreeChange.Insertion, postRoot); 92 | } 93 | 94 | function latestRoot() external view returns (uint256) { 95 | return _latestRoot; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/CommonSemaphoreTest.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.15; 2 | 3 | import {SemaphoreVerifier} from "./SemaphoreVerifier16.sol"; 4 | import {PRBTest} from "@prb/test/PRBTest.sol"; 5 | import {StdCheats} from "forge-std/StdCheats.sol"; 6 | 7 | /// @title Common Semaphore Verifier Test Contract 8 | /// @author Worldcoin 9 | /// @notice A contract that generates the shared test cases for the SemaphoreVerifier contract. 10 | contract CommonSemaphoreVerifierTest is PRBTest { 11 | /////////////////////////////////////////////////////////////////////////////// 12 | /// CONTRACT DATA /// 13 | /////////////////////////////////////////////////////////////////////////////// 14 | 15 | /// @notice The SemaphoreVerifier contract to test. 16 | SemaphoreVerifier internal verifier; 17 | 18 | /// The proof is invalid. 19 | /// @dev This can mean that provided Groth16 proof points are not on their 20 | /// curves, that pairing equation fails, or that the proof is not for the 21 | /// provided public input. 22 | error ProofInvalid(); 23 | 24 | /////////////////////////////////////////////////////////////////// 25 | /// INCLUSION /// 26 | /////////////////////////////////////////////////////////////////// 27 | /// @dev generated using https://github.com/worldcoin/semaphore-mock 28 | /// steps: 29 | /// 1. cargo run --release generate-identities --identities 10 30 | /// 2. cargo run --release prove-inclusion --identities out/random_identities.json --tree-depth 16 --identity-index 3 31 | /// @dev params from `src/test/data/InclusionProof.json` (output of step 2.) 32 | uint256 internal constant inclusionRoot = 33 | 0xdf9f0cb5a3afe2129e349c1435bfbe9e6f091832fdfa7b739b61c5db2cbdde9; 34 | uint256 internal constant inclusionSignalHash = 35 | 0xbc6bb462e38af7da48e0ae7b5cbae860141c04e5af2cf92328cd6548df111f; 36 | uint256 internal constant inclusionNullifierHash = 37 | 0x2887375654a2f83868b277f3836678aa55475fd5c840b117913ea4a7c9ded6fc; 38 | uint256 internal constant inclusionExternalNullifierHash = 39 | 0xfd3a1e9736c12a5d4a31f26362b577ccafbd523d358daf40cdc04d90e17f77; 40 | 41 | uint256[8] inclusionProof; 42 | 43 | constructor() { 44 | verifier = new SemaphoreVerifier(); 45 | // Create the inclusion proof term. 46 | // output from semaphore-mtb prove in src/test/data/InclusionProof.json 47 | // 48 | /// @dev generated using https://github.com/worldcoin/semaphore-mock 49 | /// steps: 50 | /// 1. cargo run --release generate-identities --identities 10 51 | /// 2. cargo run --release prove-inclusion --identities out/random_identities.json --tree-depth 16 --identity-index 3 52 | inclusionProof = [ 53 | 0x27d70bdecb420a7322a0e44ef68345fc67e9903a3980762c23dfda5cf4d65715, 54 | 0x1aba064ef272dd53b498d856c711890249a63a46825fe6d332fc5868ad854ef4, 55 | 0x23a76f9777710f268d2092d859344cdc8d7f77abef35695f89d1ebf771d8a520, 56 | 0x295ab87eb7c0ad9470ec2b56b35309f5e4576679ef6180ed78124e3f549f125d, 57 | 0x1da63a007225659d3a70a2dfe807df5c3e8423bfd8e059d72909a1def161573f, 58 | 0x2578db76ee9f64ff4eb0b532cb796dfa27d86ae8cd29e2d6b32f9428c71acb8b, 59 | 0xd00d49d5db4c5b11a13aca379f5c3c627a6e8fc1c4470e7a56017307aca51a2, 60 | 0xf6ee8db704ecb5c149e5a046a03e8767ba5a818c08320f6245070e4c0e99b77 61 | ]; 62 | } 63 | 64 | /////////////////////////////////////////////////////////////////////////////// 65 | /// TEST CASES /// 66 | /////////////////////////////////////////////////////////////////////////////// 67 | 68 | /// @notice Tests that Semaphore proofs verify correctly. 69 | function test_ValidProof() public view { 70 | verifier.verifyProof( 71 | inclusionProof, 72 | [ 73 | inclusionRoot, 74 | inclusionNullifierHash, 75 | inclusionSignalHash, 76 | inclusionExternalNullifierHash 77 | ] 78 | ); 79 | } 80 | 81 | /// @notice Tests that compressed Semaphore proofs verify correctly. 82 | function test_ValidCompressedProof() public view { 83 | uint256[4] memory compressedProof = verifier.compressProof(inclusionProof); 84 | 85 | verifier.verifyCompressedProof( 86 | compressedProof, 87 | [ 88 | inclusionRoot, 89 | inclusionNullifierHash, 90 | inclusionSignalHash, 91 | inclusionExternalNullifierHash 92 | ] 93 | ); 94 | } 95 | 96 | /// @notice Tests that invalid Semaphore proofs revert 97 | /// 98 | /// @param proof The proof to test. 99 | /// @param input The public input to test. 100 | function test_InvalidProof(uint256[8] calldata proof, uint256[4] calldata input) public { 101 | bool success; 102 | bytes memory returnData; 103 | 104 | vm.expectRevert(ProofInvalid.selector); 105 | (success, returnData) = address(verifier).staticcall( 106 | abi.encodeWithSelector(verifier.verifyProof.selector, proof, input) 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/PolygonWorldID.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {PolygonWorldID} from "src/PolygonWorldID.sol"; 5 | import {SemaphoreTreeDepthValidator} from "src/utils/SemaphoreTreeDepthValidator.sol"; 6 | import {PRBTest} from "@prb/test/PRBTest.sol"; 7 | import {StdCheats} from "forge-std/StdCheats.sol"; 8 | 9 | /// @title PolygonWorldIDTest 10 | /// @author Worldcoin 11 | /// @notice A test contract for PolygonWorldID 12 | /// @dev The PolygonWorldID contract is deployed on Polygon PoS and is called by the StateBridge contract. 13 | /// @dev This contract uses the Optimism CommonTest.t.sol tool suite to test the PolygonWorldID contract. 14 | contract PolygonWorldIDTest is PRBTest, StdCheats { 15 | /////////////////////////////////////////////////////////////////// 16 | /// WORLD ID /// 17 | /////////////////////////////////////////////////////////////////// 18 | /// @notice The PolygonWorldID contract 19 | PolygonWorldID internal id; 20 | 21 | /// @notice MarkleTree depth 22 | uint8 internal treeDepth = 16; 23 | 24 | /// @notice demo address 25 | address public owner = address(0x1111111); 26 | 27 | /// @notice fxChild contract address 28 | address public fxChild = address(0x2222222); 29 | 30 | /// @notice Emitted when an attempt is made to set the FxChildTunnel to the zero address. 31 | error AddressZero(); 32 | 33 | /// @notice Thrown when setFxRootTunnel is called for the first time 34 | event SetFxRootTunnel(address fxRootTunnel); 35 | 36 | /// @notice Emitted when an attempt is made to set the FxBaseChildTunnel's 37 | /// fxRootTunnel when it has already been set. 38 | error FxBaseChildRootTunnelAlreadySet(); 39 | 40 | function setUp() public { 41 | /// @notice Initialize the PolygonWorldID contract 42 | vm.prank(owner); 43 | id = new PolygonWorldID(treeDepth, fxChild); 44 | 45 | /// @dev label important addresses 46 | vm.label(address(this), "Sender"); 47 | vm.label(address(id), "PolygonWorldID"); 48 | } 49 | 50 | /////////////////////////////////////////////////////////////////// 51 | /// TESTS /// 52 | /////////////////////////////////////////////////////////////////// 53 | 54 | /// @notice Tests that the owner of the PolygonWorldID contract can transfer ownership 55 | /// using Ownable2Step transferOwnership 56 | /// @param newOwner the new owner of the contract 57 | function test_owner_transferOwnership_succeeds(address newOwner) public { 58 | vm.prank(owner); 59 | id.transferOwnership(newOwner); 60 | 61 | vm.prank(newOwner); 62 | id.acceptOwnership(); 63 | 64 | assertEq(id.owner(), newOwner); 65 | } 66 | 67 | function testConstructorWithInvalidTreeDepth(uint8 actualTreeDepth) public { 68 | // Setup 69 | vm.assume(!SemaphoreTreeDepthValidator.validate(actualTreeDepth)); 70 | vm.expectRevert(abi.encodeWithSignature("UnsupportedTreeDepth(uint8)", actualTreeDepth)); 71 | 72 | new PolygonWorldID(actualTreeDepth, fxChild); 73 | } 74 | 75 | /// @notice Checks that it is possible to get the tree depth the contract was initialized with. 76 | function testCanGetTreeDepth(uint8 actualTreeDepth) public { 77 | // Setup 78 | vm.assume(SemaphoreTreeDepthValidator.validate(actualTreeDepth)); 79 | 80 | id = new PolygonWorldID(actualTreeDepth, fxChild); 81 | 82 | // Test 83 | assert(id.getTreeDepth() == actualTreeDepth); 84 | } 85 | 86 | /// @notice Checks that calling the placeholder setRootHistoryExpiry function reverts. 87 | function testSetRootHistoryExpiryReverts(uint256 expiryTime) public { 88 | // Test 89 | vm.expectRevert( 90 | "PolygonWorldID: Root history expiry should only be set via the state bridge" 91 | ); 92 | id.setRootHistoryExpiry(expiryTime); 93 | } 94 | 95 | /// @notice checks that the owner of the PolygonWorldID contract can set the fxRootTunnel 96 | /// @param newFxRootTunnel the new fxRootTunnel 97 | function testOwnerCanSetFxRootTunnel(address newFxRootTunnel) public { 98 | vm.assume(newFxRootTunnel != address(0)); 99 | 100 | vm.expectEmit(true, true, true, true); 101 | emit SetFxRootTunnel(newFxRootTunnel); 102 | 103 | vm.prank(owner); 104 | id.setFxRootTunnel(newFxRootTunnel); 105 | } 106 | 107 | /// @notice Tests that the fxChildTunnel can't be set to the zero address 108 | function test_cannotSetFxChildTunnelToZero_reverts() public { 109 | vm.expectRevert(AddressZero.selector); 110 | 111 | vm.prank(owner); 112 | new PolygonWorldID(treeDepth, address(0)); 113 | } 114 | 115 | /// @notice tests that the FxBaseChildTunnel's fxRootTunnel can't be set once it has already been set 116 | function test_cannotSetFxRootTunnelMoreThanOnce_reverts(address _fxRootTunnel) public { 117 | vm.assume(_fxRootTunnel != address(0)); 118 | 119 | vm.prank(owner); 120 | id.setFxRootTunnel(_fxRootTunnel); 121 | 122 | vm.expectRevert(FxBaseChildRootTunnelAlreadySet.selector); 123 | 124 | vm.prank(owner); 125 | id.setFxRootTunnel(_fxRootTunnel); 126 | } 127 | 128 | /// @notice Tests that a nonPendingOwner can't accept ownership of PolygonWorldID 129 | /// @param newOwner the new owner of the contract 130 | function test_notOwner_acceptOwnership_reverts(address newOwner, address randomAddress) 131 | public 132 | { 133 | vm.assume( 134 | newOwner != address(0) && randomAddress != address(0) && randomAddress != newOwner 135 | ); 136 | 137 | vm.prank(owner); 138 | id.transferOwnership(newOwner); 139 | 140 | vm.expectRevert("Ownable2Step: caller is not the new owner"); 141 | 142 | vm.prank(randomAddress); 143 | id.acceptOwnership(); 144 | } 145 | 146 | /// @notice Tests that ownership can't be renounced 147 | function test_owner_renounceOwnership_reverts() public { 148 | vm.expectRevert(PolygonWorldID.CannotRenounceOwnership.selector); 149 | 150 | vm.prank(owner); 151 | id.renounceOwnership(); 152 | } 153 | 154 | /// @notice Tests that the fxRootTunnel can't be set to the zero address 155 | function test_cannotSetFxRootTunnelToZero_reverts() public { 156 | vm.expectRevert(AddressZero.selector); 157 | 158 | vm.prank(owner); 159 | id.setFxRootTunnel(address(0)); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/PolygonStateBridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | // Optimism interface for cross domain messaging 5 | import {IPolygonWorldID} from "./interfaces/IPolygonWorldID.sol"; 6 | import {IWorldIDIdentityManager} from "./interfaces/IWorldIDIdentityManager.sol"; 7 | import {IRootHistory} from "./interfaces/IRootHistory.sol"; 8 | import {Ownable2Step} from "openzeppelin-contracts/access/Ownable2Step.sol"; 9 | import {FxBaseRootTunnel} from "fx-portal/contracts/tunnel/FxBaseRootTunnel.sol"; 10 | 11 | /// @title Polygon World ID State Bridge 12 | /// @author Worldcoin 13 | /// @notice Distributes new World ID Identity Manager roots to World ID supported networks 14 | /// @dev This contract lives on Ethereum mainnet and is called by the World ID Identity Manager contract 15 | /// in the registerIdentities method 16 | contract PolygonStateBridge is FxBaseRootTunnel, Ownable2Step { 17 | /////////////////////////////////////////////////////////////////// 18 | /// STORAGE /// 19 | /////////////////////////////////////////////////////////////////// 20 | 21 | /// @notice WorldID Identity Manager contract 22 | IWorldIDIdentityManager public immutable worldID; 23 | 24 | /////////////////////////////////////////////////////////////////// 25 | /// EVENTS /// 26 | /////////////////////////////////////////////////////////////////// 27 | 28 | /// @notice Emitted when the StateBridge sets the root history expiry for OpWorldID and PolygonWorldID 29 | /// @param rootHistoryExpiry The new root history expiry 30 | event SetRootHistoryExpiry(uint256 rootHistoryExpiry); 31 | 32 | /// @notice Emitted when the owner calls setFxChildTunnel for the first time 33 | event SetFxChildTunnel(address fxChildTunnel); 34 | 35 | /// @notice Emitted when a root is sent to PolygonWorldID 36 | /// @param root The latest WorldID Identity Manager root. 37 | event RootPropagated(uint256 root); 38 | 39 | /////////////////////////////////////////////////////////////////// 40 | /// ERRORS /// 41 | /////////////////////////////////////////////////////////////////// 42 | 43 | /// @notice Emitted when an attempt is made to renounce ownership. 44 | error CannotRenounceOwnership(); 45 | 46 | /// @notice Emitted when an attempt is made to set the FxBaseRootTunnel, 47 | /// FxChildTunnel, CheckpointManager or WorldIDIdentityManager addresses to the zero address. 48 | error AddressZero(); 49 | 50 | /// @notice Emitted when an attempt is made to set the FxBaseRootTunnel's 51 | /// fxChildTunnel when it has already been set. 52 | error FxBaseRootChildTunnelAlreadySet(); 53 | 54 | /////////////////////////////////////////////////////////////////// 55 | /// CONSTRUCTOR /// 56 | /////////////////////////////////////////////////////////////////// 57 | 58 | /// @notice constructor 59 | /// @param _checkpointManager address of the checkpoint manager contract 60 | /// @param _fxRoot address of Polygon's fxRoot contract, part of the FxPortal bridge (Goerli or Mainnet) 61 | /// @param _worldIDIdentityManager Deployment address of the WorldID Identity Manager contract 62 | /// @custom:reverts AddressZero If any of the constructor arguments are the zero address 63 | constructor(address _checkpointManager, address _fxRoot, address _worldIDIdentityManager) 64 | FxBaseRootTunnel(_checkpointManager, _fxRoot) 65 | { 66 | if ( 67 | _checkpointManager == address(0) || _fxRoot == address(0) 68 | || _worldIDIdentityManager == address(0) 69 | ) { 70 | revert AddressZero(); 71 | } 72 | 73 | worldID = IWorldIDIdentityManager(_worldIDIdentityManager); 74 | } 75 | 76 | /////////////////////////////////////////////////////////////////// 77 | /// PUBLIC API /// 78 | /////////////////////////////////////////////////////////////////// 79 | 80 | /// @notice Sends the latest WorldIDIdentityManager root 81 | /// to Polygon's StateChild contract (PolygonWorldID) 82 | function propagateRoot() external { 83 | uint256 latestRoot = worldID.latestRoot(); 84 | 85 | bytes memory message = abi.encodeCall(IPolygonWorldID.receiveRoot, (latestRoot)); 86 | 87 | /// @notice FxBaseRootTunnel method to send bytes payload to FxBaseChildTunnel contract 88 | _sendMessageToChild(message); 89 | 90 | emit RootPropagated(latestRoot); 91 | } 92 | 93 | /// @notice Sets the root history expiry for PolygonWorldID 94 | /// @param _rootHistoryExpiry The new root history expiry 95 | function setRootHistoryExpiryPolygon(uint256 _rootHistoryExpiry) external onlyOwner { 96 | bytes memory message = 97 | abi.encodeCall(IRootHistory.setRootHistoryExpiry, (_rootHistoryExpiry)); 98 | 99 | /// @notice FxBaseRootTunnel method to send bytes payload to FxBaseChildTunnel contract 100 | _sendMessageToChild(message); 101 | 102 | emit SetRootHistoryExpiry(_rootHistoryExpiry); 103 | } 104 | 105 | /// @notice boilerplate function to satisfy FxBaseRootTunnel inheritance (not going to be used) 106 | function _processMessageFromChild(bytes memory) internal override { 107 | /// WorldID 🌎🆔 State Bridge 108 | } 109 | 110 | /////////////////////////////////////////////////////////////////////////////// 111 | /// ADDRESS MANAGEMENT /// 112 | /////////////////////////////////////////////////////////////////////////////// 113 | 114 | /// @notice Sets the `fxChildTunnel` address if not already set. 115 | /// @dev This implementation replicates the logic from `FxBaseRootTunnel` due to the inability 116 | /// to call `external` superclass methods when overriding them. 117 | /// 118 | /// @param _fxChildTunnel The address of the child (non-L1) tunnel contract. 119 | /// 120 | /// @custom:reverts string If the root tunnel has already been set. 121 | /// @custom:reverts AddressZero If the `_fxChildTunnel` is the zero address. 122 | function setFxChildTunnel(address _fxChildTunnel) public virtual override onlyOwner { 123 | if (fxChildTunnel != address(0x0)) { 124 | revert FxBaseRootChildTunnelAlreadySet(); 125 | } 126 | 127 | if (_fxChildTunnel == address(0x0)) { 128 | revert AddressZero(); 129 | } 130 | 131 | fxChildTunnel = _fxChildTunnel; 132 | 133 | emit SetFxChildTunnel(_fxChildTunnel); 134 | } 135 | 136 | /////////////////////////////////////////////////////////////////// 137 | /// OWNERSHIP /// 138 | /////////////////////////////////////////////////////////////////// 139 | /// @notice Ensures that ownership of WorldID implementations cannot be renounced. 140 | /// @dev This function is intentionally not `virtual` as we do not want it to be possible to 141 | /// renounce ownership for any WorldID implementation. 142 | /// @dev This function is marked as `onlyOwner` to maintain the access restriction from the base 143 | /// contract. 144 | function renounceOwnership() public view override onlyOwner { 145 | revert CannotRenounceOwnership(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/utils/BytesUtils.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.15; 2 | 3 | library BytesUtils { 4 | /// @notice Emitted when the payload is too short to contain a selector (at least 4 bytes). 5 | error PayloadTooShort(); 6 | 7 | /// @notice grabSelector, takes a byte array _payload as input and returns the first 4 bytes 8 | /// of the array as a bytes4 value _selector. The function uses EVM assembly language 9 | /// to load the 4-byte selector from the _payload array and then shift it left by 224 bits 10 | /// (0xE0 in hexadecimal) to get the correct value. 11 | /// @param _payload The byte array from which to extract the selector 12 | /// @return _selector The first 4 bytes of the _payload array (the function selector from encodeWithSignature) 13 | /// @dev This function is currently unused 14 | function grabSelector(bytes memory _payload) internal pure returns (bytes4 _selector) { 15 | if (_payload.length < 4) { 16 | revert PayloadTooShort(); 17 | } 18 | assembly ("memory-safe") { 19 | /// @dev uses mload to load the first 32 bytes of _payload 20 | /// (starting at memory address _payload + 0x20) into memory, 21 | /// then shr to shift the loaded value right by 224 bits 22 | /// (0xE0 in hexadecimal). Therefore only the last 4 bytes (32 bits remain), 23 | /// and finally we pad the value to the left by using shl to shift 24 | /// the by 224 bits to the left to get the correct value for _selector. 25 | _selector := shl(0xE0, shr(0xE0, mload(add(_payload, 0x20)))) 26 | } 27 | } 28 | 29 | /// @notice stripSelector, takes a byte array _payload as input and returns a new byte array 30 | /// _payloadData that contains all the data in _payload except for the first 4 bytes (the selector). 31 | /// The function first allocates a new block of memory to store the new byte array, then copies the 32 | /// length of the original _payload array (minus 4 bytes) into the new array, and then copies the 33 | /// remaining data from the original _payload array into the new array, starting from the fifth byte. 34 | /// The function then updates the free memory pointer to account for the new memory allocation. 35 | /// @param _payload The byte array from which to extract the payload data 36 | /// @return _payloadData The payload data from the _payload array 37 | /// (payload minus selector which is 4 bytes long) 38 | /// @dev This function is currently unused 39 | function stripSelector(bytes memory _payload) 40 | internal 41 | pure 42 | returns (bytes memory _payloadData) 43 | { 44 | if (_payload.length <= 4) { 45 | revert PayloadTooShort(); 46 | } 47 | assembly ("memory-safe") { 48 | // Grab the pointer to some free memory 49 | _payloadData := mload(0x40) 50 | 51 | // Copy the length - 4 52 | let newLength := sub(mload(_payload), 0x04) 53 | mstore(_payloadData, newLength) 54 | 55 | // Copy the data following the selector 56 | /// @dev These lines copy the length of the original _payload array 57 | /// (minus 4 bytes for the selector) into the first 32 bytes of the new 58 | /// _payloadData array. Specifically, it uses mload to load the value stored 59 | /// at memory address _payload, which is the length of the _payload array, 60 | /// and then sub to subtract 4 from this value to get the correct length for 61 | /// _payloadData. Finally, it uses mstore to store this value at memory address _payloadData. 62 | let dataStart := add(_payloadData, 0x20) 63 | let payloadStart := add(_payload, 0x24) 64 | for { let i := 0x00 } lt(i, mload(_payload)) { i := add(i, 0x20) } { 65 | mstore(add(dataStart, i), mload(add(payloadStart, i))) 66 | } 67 | 68 | // Account for the full length of the copied data 69 | // length word + data length 70 | let fullLength := add(newLength, 0x20) 71 | 72 | // Update the free memory pointer 73 | mstore(0x40, add(_payloadData, and(add(fullLength, 0x1F), not(0x1F)))) 74 | 75 | // Compute the last 32-byte aligned memory address of the copied data 76 | let lastAlignedAddr := add(dataStart, and(newLength, not(0x1F))) 77 | 78 | // Compute the number of bytes beyond the end of the copied data 79 | let endBytes := sub(newLength, sub(lastAlignedAddr, dataStart)) 80 | 81 | // Load the last 32-byte word of the copied data 82 | let lastWord := mload(lastAlignedAddr) 83 | 84 | // Zero out any erroneously copied bits beyond the end of the payload data 85 | let mask := sub(shl(endBytes, 0x8), 0x1) 86 | lastWord := and(lastWord, not(mask)) 87 | 88 | // Store the modified word back to memory 89 | mstore(lastAlignedAddr, lastWord) 90 | } 91 | } 92 | 93 | /// @custom:fnauthor ENS 94 | /// @custom:source https://github.com/ensdomains/ens-contracts/blob/09f44a985c901bf86a1c6d00f78c51086d7b9afd/contracts/dnssec-oracle/BytesUtils.sol#L294-L318 95 | /// @dev Copies a substring into a new byte string. Used in PolygonWorldID.sol 96 | /// @param self The byte string to copy from. 97 | /// @param offset The offset to start copying at. 98 | /// @param len The number of bytes to copy. 99 | function substring(bytes memory self, uint256 offset, uint256 len) 100 | internal 101 | pure 102 | returns (bytes memory) 103 | { 104 | // checks that we don't overflow (write past self) 105 | require(offset + len <= self.length); 106 | 107 | // allocates new bytes array with specified length 108 | bytes memory ret = new bytes(len); 109 | uint256 dest; 110 | uint256 src; 111 | 112 | // sets pointers to memory blocks (first 32 bytes store length) 113 | assembly { 114 | dest := add(ret, 32) 115 | // offsets self by specified number of bytes and stores it in source 116 | src := add(add(self, 32), offset) 117 | } 118 | 119 | // copies specified length of bytes from source (starts at self + offset) to dest 120 | memcpy(dest, src, len); 121 | 122 | // return substring 123 | return ret; 124 | } 125 | 126 | /// @custom:fnauthor ENS 127 | /// @custom:source https://github.com/ensdomains/ens-contracts/blob/09f44a985c901bf86a1c6d00f78c51086d7b9afd/contracts/dnssec-oracle/BytesUtils.sol#LL273C4-L292C6 128 | /// @dev Copies a memory block to another memory block. 129 | /// @param dest The destination memory pointer. 130 | /// @param src The source memory pointer. 131 | /// @param len The length of the memory block to copy. 132 | function memcpy(uint256 dest, uint256 src, uint256 len) private pure { 133 | // Copy word-length chunks while possible 134 | for (; len >= 32; len -= 32) { 135 | // each 32 bytes 136 | assembly { 137 | // store the current 32 bytes from src into dest 138 | mstore(dest, mload(src)) 139 | } 140 | // move forward by 32 bytes 141 | dest += 32; 142 | src += 32; 143 | } 144 | 145 | // Copy remaining bytes (0 < len < 32) 146 | unchecked { 147 | // create mask to zero out bytes that we don't want to overwrite 148 | uint256 mask = (256 ** (32 - len)) - 1; 149 | assembly { 150 | // apply mask to source 151 | let srcpart := and(mload(src), not(mask)) 152 | // apply mask to destination 153 | let destpart := and(mload(dest), mask) 154 | // store the result of ORing the two together in dest 155 | // (will not copy bytes that we don't want to overwrite) 156 | mstore(dest, or(destpart, srcpart)) 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/PolygonWorldID.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {WorldIDBridge} from "./abstract/WorldIDBridge.sol"; 5 | import {FxBaseChildTunnel} from "fx-portal/contracts/tunnel/FxBaseChildTunnel.sol"; 6 | import {Ownable2Step} from "openzeppelin-contracts/access/Ownable2Step.sol"; 7 | import {BytesUtils} from "./utils/BytesUtils.sol"; 8 | 9 | /// @title Polygon WorldID Bridge 10 | /// @author Worldcoin 11 | /// @notice A contract that manages the root history of the WorldID merkle root on Polygon PoS. 12 | /// @dev This contract is deployed on Polygon PoS and is called by the StateBridge contract for each 13 | /// new root insertion. 14 | /// @dev Ownable2Step allows for transferOwnership to the zero address 15 | contract PolygonWorldID is WorldIDBridge, FxBaseChildTunnel, Ownable2Step { 16 | /////////////////////////////////////////////////////////////////// 17 | /// STORAGE /// 18 | /////////////////////////////////////////////////////////////////// 19 | 20 | /// @notice The selector of the `receiveRoot` function. 21 | /// @dev this selector is precomputed in the constructor to not have to recompute them for every 22 | /// call of the _processMesageFromRoot function 23 | bytes4 private immutable receiveRootSelector = bytes4(keccak256("receiveRoot(uint256)")); 24 | 25 | /// @notice The selector of the `receiveRootHistoryExpiry` function. 26 | /// @dev this selector is precomputed in the constructor to not have to recompute them for every 27 | /// call of the _processMesageFromRoot function 28 | bytes4 private immutable receiveRootHistoryExpirySelector = 29 | bytes4(keccak256("setRootHistoryExpiry(uint256)")); 30 | 31 | /////////////////////////////////////////////////////////////////// 32 | /// EVENTS /// 33 | /////////////////////////////////////////////////////////////////// 34 | /// @notice Thrown when setFxRootTunnel is called for the first time 35 | event SetFxRootTunnel(address fxRootTunnel); 36 | 37 | /////////////////////////////////////////////////////////////////// 38 | /// ERRORS /// 39 | /////////////////////////////////////////////////////////////////// 40 | 41 | /// @notice Emitted when the message selector passed from FxRoot is invalid. 42 | error InvalidMessageSelector(bytes4 selector); 43 | 44 | /// @notice Emitted when an attempt is made to renounce ownership. 45 | error CannotRenounceOwnership(); 46 | 47 | /// @notice Emitted when an attempt is made to set the FxBaseChildTunnel or 48 | /// the FxRoot Tunnel to the zero address. 49 | error AddressZero(); 50 | 51 | /// @notice Emitted when an attempt is made to set the FxBaseChildTunnel's 52 | /// fxRootTunnel when it has already been set. 53 | error FxBaseChildRootTunnelAlreadySet(); 54 | 55 | /////////////////////////////////////////////////////////////////////////////// 56 | /// CONSTRUCTION /// 57 | /////////////////////////////////////////////////////////////////////////////// 58 | 59 | /// @notice Initializes the contract's storage variables with the correct parameters 60 | /// 61 | /// @param _treeDepth The depth of the WorldID Identity Manager merkle tree. 62 | /// @param _fxChild The address of the FxChild tunnel - the contract that will receive messages on Polygon 63 | /// and Broadcasts them to FxPortal which bridges the messages to Ethereum 64 | constructor(uint8 _treeDepth, address _fxChild) 65 | WorldIDBridge(_treeDepth) 66 | FxBaseChildTunnel(_fxChild) 67 | { 68 | if (address(_fxChild) == address(0)) { 69 | revert AddressZero(); 70 | } 71 | } 72 | 73 | /////////////////////////////////////////////////////////////////////////////// 74 | /// ROOT MIRRORING /// 75 | /////////////////////////////////////////////////////////////////////////////// 76 | 77 | /// @notice An internal function used to receive messages from the StateBridge contract. 78 | /// @dev Calls `receiveRoot` upon receiving a message from the StateBridge contract via the 79 | /// FxChildTunnel. Can revert if the message is not valid - decoding fails. 80 | /// Can not work if Polygon's StateSync mechanism breaks and FxPortal does not receive the message 81 | /// on the other end. 82 | /// @dev the message payload has the following format: 83 | /// `bytes4 selector` + `bytes payload`, the first 4 bytes are extracted using BytesUtils, once the 84 | /// selector is extracted, the payload is decoded using abi.decode(payload, (uint256)) 85 | /// 86 | /// @custom:param uint256 stateId An unused placeholder variable for `stateId`, 87 | /// required by the signature in fxChild. 88 | /// @param sender The sender of the message. 89 | /// @param message An ABI-encoded tuple of `(uint256 newRoot, uint128 supersedeTimestamp)` that 90 | /// is used to call `receiveRoot`. 91 | /// 92 | /// @custom:reverts string If the sender is not valid. 93 | /// @custom:reverts EvmError If the provided `message` does not match the expected format. 94 | function _processMessageFromRoot(uint256, address sender, bytes memory message) 95 | internal 96 | override 97 | validateSender(sender) 98 | { 99 | bytes4 selector = bytes4(BytesUtils.substring(message, 0, 4)); 100 | bytes memory payload = BytesUtils.substring(message, 4, message.length - 4); 101 | 102 | if (selector == receiveRootSelector) { 103 | uint256 root = abi.decode(payload, (uint256)); 104 | _receiveRoot(root); 105 | } else if (selector == receiveRootHistoryExpirySelector) { 106 | uint256 rootHistoryExpiry = abi.decode(payload, (uint256)); 107 | _setRootHistoryExpiry(rootHistoryExpiry); 108 | } else { 109 | revert InvalidMessageSelector(selector); 110 | } 111 | } 112 | 113 | /////////////////////////////////////////////////////////////////////////////// 114 | /// DATA MANAGEMENT /// 115 | /////////////////////////////////////////////////////////////////////////////// 116 | 117 | /// @notice Placeholder to satisfy WorldIDBridge inheritance 118 | /// @dev This function is not used on Polygon PoS because of FxPortal message passing architecture 119 | function setRootHistoryExpiry(uint256) public virtual override { 120 | revert("PolygonWorldID: Root history expiry should only be set via the state bridge"); 121 | } 122 | 123 | /////////////////////////////////////////////////////////////////////////////// 124 | /// TUNNEL MANAGEMENT /// 125 | /////////////////////////////////////////////////////////////////////////////// 126 | 127 | /// @notice Sets the `fxRootTunnel` address if not already set. 128 | /// @dev This implementation replicates the logic from `FxBaseChildTunnel` due to the inability 129 | /// to call `external` superclass methods when overriding them. 130 | /// 131 | /// @param _fxRootTunnel The address of the root (L1) tunnel contract. 132 | /// 133 | /// @custom:reverts string If the root tunnel has already been set. 134 | function setFxRootTunnel(address _fxRootTunnel) external virtual override onlyOwner { 135 | if (fxRootTunnel != address(0)) { 136 | revert FxBaseChildRootTunnelAlreadySet(); 137 | } 138 | 139 | if (_fxRootTunnel == address(0x0)) { 140 | revert AddressZero(); 141 | } 142 | 143 | fxRootTunnel = _fxRootTunnel; 144 | 145 | emit SetFxRootTunnel(_fxRootTunnel); 146 | } 147 | 148 | /////////////////////////////////////////////////////////////////// 149 | /// OWNERSHIP /// 150 | /////////////////////////////////////////////////////////////////// 151 | /// @notice Ensures that ownership of WorldID implementations cannot be renounced. 152 | /// @dev This function is intentionally not `virtual` as we do not want it to be possible to 153 | /// renounce ownership for any WorldID implementation. 154 | /// @dev This function is marked as `onlyOwner` to maintain the access restriction from the base 155 | /// contract. 156 | function renounceOwnership() public view override onlyOwner { 157 | revert CannotRenounceOwnership(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # world-id-state-bridge 2 | 3 | ![spec](docs/state-bridge.svg) 4 | 5 | ## Description 6 | 7 | State bridge between the WorldID Ethereum mainnet deployment and WorldID supported networks. The [spec](./docs/spec.md) 8 | can be found in `docs/spec.md`. 9 | 10 | ## Deployments 11 | 12 | The addresses of the contract deployments for production and staging can be found in 13 | [`docs/deployments.md`](./docs/deployments.md#production). 14 | 15 | ## Supported networks 16 | 17 | Currently, the supported networks are Polygon PoS (for backwards compatibility), Optimism and Base. The next iteration 18 | of the bridge will most-likely be based on storage proofs and will support most EVM networks off the get-go, with other 19 | networks pending storage proof verifier and Semaphore verifier implementations and deployments. 20 | 21 | ### Future integrations 22 | 23 | If you want to support World ID on your network, please reach out to us by opening a GitHub issue. To support World ID 24 | the current requirements are: 25 | 26 | - have a native L1<>L2 bridge with Ethereum mainnet and Ethereum Goerli/Sepolia (for testnet integration tests) 27 | - your network needs to have an EVM execution environment (like Optimism, Arbitrum, Scroll, Polygon zkEVM, zkSync Era, 28 | etc) 29 | 30 | If your network meets these requirements, please reach out to us and we will work with you to integrate World ID. In the 31 | mid-term future we plan on supporting storage proof solutions like [Axiom](https://axiom.xyz/) and 32 | [Herodotus](https://herodotus.dev/) to bridge World ID state to other networks more seamlessly and with better UX and 33 | for cheaper costs. Currently each bridge incurs the cost of each cross-chain message sent by calling the 34 | `propagateRoot()` function. Subsidizing these costs on your end would make the integration process very simple. 35 | 36 | If your network is not EVM-compatible/equivalent, a new implementation of the World ID contracts will need to be done 37 | for your execution environment. Requirements are: 38 | 39 | - Have a way to implement the `SemaphoreVerifier` and `semaphore-mtb` circuits verifier contracts which verify `Groth16` 40 | proofs over the `BN254` curve. 41 | - Have the capabilities to support all cryptographic primitives that will be implemented in future versions of World ID 42 | as time goes on. 43 | - Support the primitives needed to verify Axiom or Herodotus storage proofs. 44 | 45 | For L1 to L1 bridges, there are solutions like [Succinct](https://succinct.xyz/)'s 46 | [Telepathy](https://www.telepathy.xyz/) which have weaker security guarantees than storage proofs or native L1<>L2 47 | bridges, but possibly allow for a World ID integration, provided that the reqirements above are met. 48 | 49 | ## Documentation 50 | 51 | Run `make doc` to build and deploy a simple documentation webpage on [localhost:3000](https://localhost:3000). Uses 52 | [`forge doc`](https://book.getfoundry.sh/reference/forge/forge-doc#forge-doc) under the hood and sources information 53 | from the `world-id-state-bridge` contracts [NatSpec](https://docs.soliditylang.org/en/latest/natspec-format.html) 54 | documentation. 55 | 56 | ## Usage 57 | 58 | ### Install Dependencies 59 | 60 | ```sh 61 | make install 62 | ``` 63 | 64 | ### Build 65 | 66 | Build the contracts: 67 | 68 | ```sh 69 | make build 70 | ``` 71 | 72 | ### Clean 73 | 74 | Delete the build artifacts and cache directories: 75 | 76 | ```sh 77 | make clean 78 | ``` 79 | 80 | ### Coverage 81 | 82 | Get a test coverage report: 83 | 84 | ```sh 85 | make coverage 86 | ``` 87 | 88 | ### Format 89 | 90 | Format the contracts with `forge fmt` and the rest of the files (.js, .md) with Prettier: 91 | 92 | ```sh 93 | make format 94 | ``` 95 | 96 | ### Gas Usage 97 | 98 | Get a gas report: 99 | 100 | ```sh 101 | make snapshot 102 | ``` 103 | 104 | ```sh 105 | make bench 106 | ``` 107 | 108 | ### Lint 109 | 110 | Lint the contracts: 111 | 112 | ```sh 113 | make lint 114 | ``` 115 | 116 | ### Test 117 | 118 | Run the tests: 119 | 120 | ```sh 121 | make test 122 | ``` 123 | 124 | ### Deploy 125 | 126 | Deploy the WorldID state bridge and all its components to Ethereum mainnet using the CLI tool. For a more detailed 127 | description of the deployment process and production and staging (testnet) contract addresses, see 128 | [deployments.md](./docs/deployments.md) . 129 | 130 | Integration with full system: 131 | 132 | 1. Deploy [`world-id-contracts`](https://github.com/worldcoin/world-id-contracts) 133 | 2. Get deployment address for `WorldIDIdentityManager` 134 | 3. (optional) you can also use the `WorldIDIdentityManager` address that can be found in 135 | [`docs/deployments.md`](./docs/deployments.md) to bridge existing roots. 136 | 4. Deploy [`world-id-state-bridge`](https://github.com/worldcoin/world-id-state-bridge) by running `make deploy-testnet` 137 | (requires 2.) 138 | 5. Start inserting identities into the 139 | [`WorldIDIdentityManager`](https://github.com/worldcoin/world-id-contracts/blob/main/src/WorldIDIdentityManagerImplV1.sol) 140 | 6. Propagate roots from each StateBridge contract (`OpStateBridge` on Optimism and Base and `PolygonStateBridge`) to 141 | their bridged World ID counterparts (`OpWorldID` on Base and Optimism and `PolygonWorldID`) by individually calling 142 | `propagateRoot()` on each bridge contract using 143 | [`state-bridge-relay`](https://github.com/worldcoin/state-bridge-relay) or any other method of calling the public 144 | `propagateRoot()` functions on the respective state bridges. You can monitor `PolygonWorldID` and `OpWorldID` 145 | (Optimism/Base) for root updates either using a block explorer or some other monitoring service (Tenderly, 146 | OpenZeppelin Sentinel, DataDog, ...). 147 | 7. Try and create a proof and call `verifyProof` on `OpWorldID` (Optimism/Base) or `PolygonWorldID` to check whether 148 | everything works. 149 | 150 | **Note:** Remember to call all functions that change state on these contracts via the owner address, which is the 151 | deployer address by default. 152 | 153 | #### Testnet 154 | 155 | Deploy the WorldID state bridge and all its components to the Goerli testnet. 156 | 157 | Integration with full system: 158 | 159 | 1. Deploy [`world-id-contracts`](https://github.com/worldcoin/world-id-contracts) 160 | 2. Get deployment address for `WorldIDIdentityManager` 161 | 3. (optional) you can also use the `WorldIDIdentityManager` address that can be found in 162 | [`docs/deployments.md`](./docs/deployments.md) to bridge existing roots. 163 | 4. Deploy [`world-id-state-bridge`](https://github.com/worldcoin/world-id-state-bridge) by running `make deploy-testnet` 164 | (requires 2.) 165 | 5. Start inserting identities into the 166 | [`WorldIDIdentityManager`](https://github.com/worldcoin/world-id-contracts/blob/main/src/WorldIDIdentityManagerImplV1.sol) 167 | 6. Propagate roots from each StateBridge contract (`OpStateBridge` on Optimism and Base and `PolygonStateBridge`) to 168 | their bridged World ID counterparts (`OpWorldID` on Base and Optimism and `PolygonWorldID`) by individually calling 169 | `propagateRoot()` on each bridge contract using 170 | [`state-bridge-relay`](https://github.com/worldcoin/state-bridge-relay) or any other method of calling the public 171 | `propagateRoot()` functions on the respective state bridges. You can monitor `PolygonWorldID` and `OpWorldID` 172 | (Optimism/Base) for root updates either using a block explorer or some other monitoring service (Tenderly, 173 | OpenZeppelin Sentinel, DataDog, ...). 174 | 7. Try and create a proof and call `verifyProof` on `OpWorldID` (Optimism/Base) or `PolygonWorldID` to check whether 175 | everything works. 176 | 177 | **Note:** Remember to call all functions that change state on these contracts via the owner address, which is the 178 | deployer address by default. 179 | 180 | #### Mock 181 | 182 | Deploy the WorldID state bridge and a mock WorldID identity manager to the Goerli testnet for integration tests. 183 | 184 | ```bash 185 | # to do a mock of WorlIDIdentityManager and test bridge contracts on Goerli 186 | make mock 187 | ``` 188 | 189 | #### Local Mock 190 | 191 | For local mock tests use localhost:8545 as the RPC URL (or just hit enter to use it by default) and use any non-empty 192 | string as the Etherscan API key (or just hit enter to use a placeholder by default). 193 | 194 | Run a local anvil instance to deploy the contracts locally: 195 | 196 | ```bash 197 | anvil --mnemonic --network goerli --deploy 198 | ``` 199 | 200 | ```bash 201 | # meant for local testing, deploys mock contracts to localhost 202 | make local-mock 203 | ``` 204 | 205 | ## Credits 206 | 207 | This repo uses Paul Razvan Berg's [foundry template](https://github.com/paulrberg/foundry-template/): A Foundry-based 208 | template for developing Solidity smart contracts, with sensible defaults. 209 | -------------------------------------------------------------------------------- /src/test/PolygonStateBridge.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {PolygonStateBridge} from "src/PolygonStateBridge.sol"; 5 | import {MockWorldIDIdentityManager} from "src/mock/MockWorldIDIdentityManager.sol"; 6 | 7 | import {PRBTest} from "@prb/test/PRBTest.sol"; 8 | import {StdCheats} from "forge-std/StdCheats.sol"; 9 | 10 | /// @title State Bridge Test 11 | /// @author Worldcoin 12 | /// @notice A test contract for StateBridge.sol 13 | contract PolygonStateBridgeTest is PRBTest, StdCheats { 14 | /////////////////////////////////////////////////////////////////// 15 | /// STORAGE CONFIG /// 16 | /////////////////////////////////////////////////////////////////// 17 | uint256 public mainnetFork; 18 | string private MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL"); 19 | 20 | /// @notice emitted if there is no CrossDomainMessenger contract deployed on the fork 21 | error invalidCrossDomainMessengerFork(); 22 | 23 | PolygonStateBridge polygonStateBridge; 24 | MockWorldIDIdentityManager public mockWorldID; 25 | 26 | uint32 public opGasLimit; 27 | 28 | address public mockWorldIDAddress; 29 | 30 | address public fxRoot; 31 | address public checkpointManager; 32 | address public owner; 33 | uint256 public sampleRoot; 34 | 35 | /////////////////////////////////////////////////////////////////// 36 | /// EVENTS /// 37 | /////////////////////////////////////////////////////////////////// 38 | 39 | /// @notice OpenZeppelin Ownable.sol transferOwnership event 40 | /// @param previousOwner The previous owner of the StateBridge contract 41 | /// @param newOwner The new owner of the StateBridge contract 42 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 43 | 44 | /// @notice OpenZeppelin Ownable2Step transferOwnership event 45 | /// @param previousOwner The previous owner of the StateBridge contract 46 | /// @param newOwner The new owner of the StateBridge contract 47 | event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); 48 | 49 | /// @notice Emitted when the the StateBridge sets the root history expiry for OpWorldID and PolygonWorldID 50 | /// @param rootHistoryExpiry The new root history expiry 51 | event SetRootHistoryExpiry(uint256 rootHistoryExpiry); 52 | 53 | // @notice Emitted when the owner calls setFxChildTunnel for the first time 54 | event SetFxChildTunnel(address fxChildTunnel); 55 | 56 | /// @notice Emitted when a root is sent to PolygonWorldID 57 | /// @param root The latest WorldID Identity Manager root. 58 | event RootPropagated(uint256 root); 59 | 60 | /// @notice Emitted when an attempt is made to set the FxChildTunnel to the zero address. 61 | error AddressZero(); 62 | 63 | /// @notice Emitted when an attempt is made to set the FxBaseRootTunnel's 64 | /// fxChildTunnel when it has already been set. 65 | error FxBaseRootChildTunnelAlreadySet(); 66 | 67 | function setUp() public { 68 | /// @notice Create a fork of the Ethereum mainnet 69 | mainnetFork = vm.createFork(MAINNET_RPC_URL); 70 | 71 | vm.selectFork(mainnetFork); 72 | /// @notice Roll the fork to a block where FXPortal already has been deployed 73 | /// @notice and the Base crossDomainMessenger ResolvedDelegateProxy target address is initialized 74 | vm.rollFork(17711915); 75 | 76 | sampleRoot = uint256(0x123); 77 | mockWorldID = new MockWorldIDIdentityManager(sampleRoot); 78 | mockWorldIDAddress = address(mockWorldID); 79 | 80 | checkpointManager = address(0x86E4Dc95c7FBdBf52e33D563BbDB00823894C287); 81 | fxRoot = address(0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2); 82 | 83 | polygonStateBridge = new PolygonStateBridge(checkpointManager, fxRoot, mockWorldIDAddress); 84 | 85 | owner = polygonStateBridge.owner(); 86 | } 87 | 88 | /////////////////////////////////////////////////////////////////// 89 | /// SUCCEEDS /// 90 | /////////////////////////////////////////////////////////////////// 91 | 92 | /// @notice select a specific fork 93 | function test_canSelectFork_succeeds() public { 94 | // select the fork 95 | vm.selectFork(mainnetFork); 96 | assertEq(vm.activeFork(), mainnetFork); 97 | } 98 | 99 | /// @notice tests that a root can be sent successfully to Polygon 100 | function test_propagateRoot_succeeds(address randomAddress) public { 101 | vm.assume(randomAddress != address(0) && randomAddress != owner); 102 | 103 | vm.expectEmit(true, true, true, true); 104 | 105 | emit RootPropagated(sampleRoot); 106 | 107 | vm.prank(randomAddress); 108 | polygonStateBridge.propagateRoot(); 109 | } 110 | 111 | /// @notice Tests that the owner of the StateBridge contract can transfer ownership 112 | /// using Ownable2Step transferOwnership 113 | /// @param newOwner the new owner of the contract 114 | function test_owner_transferOwnership_succeeds(address newOwner) public { 115 | vm.assume(newOwner != address(0) && newOwner != owner); 116 | 117 | vm.expectEmit(true, true, true, true); 118 | 119 | // OpenZeppelin Ownable2Step transferOwnershipStarted event 120 | emit OwnershipTransferStarted(owner, newOwner); 121 | 122 | vm.prank(owner); 123 | polygonStateBridge.transferOwnership(newOwner); 124 | 125 | vm.expectEmit(true, true, true, true); 126 | 127 | // OpenZeppelin Ownable2Step transferOwnership event 128 | emit OwnershipTransferred(owner, newOwner); 129 | 130 | vm.prank(newOwner); 131 | polygonStateBridge.acceptOwnership(); 132 | 133 | assertEq(polygonStateBridge.owner(), newOwner); 134 | } 135 | 136 | /// @notice tests whether the StateBridge contract can set root history expiry on Optimism and Polygon 137 | /// @param _rootHistoryExpiry The new root history expiry for OpWorldID and PolygonWorldID 138 | function test_owner_setRootHistoryExpiryPolygon_succeeds(uint256 _rootHistoryExpiry) public { 139 | vm.assume(owner != address(0)); 140 | 141 | vm.expectEmit(true, true, true, true); 142 | emit SetRootHistoryExpiry(_rootHistoryExpiry); 143 | 144 | vm.prank(owner); 145 | polygonStateBridge.setRootHistoryExpiryPolygon(_rootHistoryExpiry); 146 | } 147 | 148 | /// @notice tests that the owner of the StateBridge contract can set the fxChildTunnel 149 | /// @param newFxChildTunnel the new fxChildTunnel 150 | function test_owner_setFxChildTunnel(address newFxChildTunnel) public { 151 | vm.assume(newFxChildTunnel != address(0)); 152 | 153 | vm.expectEmit(true, true, true, true); 154 | emit SetFxChildTunnel(newFxChildTunnel); 155 | 156 | vm.prank(owner); 157 | polygonStateBridge.setFxChildTunnel(newFxChildTunnel); 158 | } 159 | 160 | /////////////////////////////////////////////////////////////////// 161 | /// REVERTS /// 162 | /////////////////////////////////////////////////////////////////// 163 | 164 | /// @notice tests that the StateBridge contract can't be constructed with a zero address for params 165 | function test_constructorParamsCannotBeZeroAddresses_reverts() public { 166 | vm.expectRevert(AddressZero.selector); 167 | polygonStateBridge = new PolygonStateBridge(checkpointManager, fxRoot, address(0)); 168 | 169 | vm.expectRevert(AddressZero.selector); 170 | polygonStateBridge = 171 | new PolygonStateBridge(checkpointManager, address(0), mockWorldIDAddress); 172 | 173 | vm.expectRevert(AddressZero.selector); 174 | polygonStateBridge = new PolygonStateBridge(address(0), fxRoot, mockWorldIDAddress); 175 | } 176 | 177 | /// @notice tests that the FxChildTunnel can't be set to the zero address 178 | function test_setFxChildCannotBeZeroAddress_reverts() public { 179 | vm.expectRevert(AddressZero.selector); 180 | 181 | vm.prank(owner); 182 | polygonStateBridge.setFxChildTunnel(address(0)); 183 | } 184 | 185 | /// @notice tests that the FxBaseRootTunnel's fxChildTunnel can't be set once it has already been set 186 | function test_cannotSetFxChildTunnelMoreThanOnce_reverts(address _fxChildTunnel) public { 187 | vm.assume(_fxChildTunnel != address(0)); 188 | 189 | vm.prank(owner); 190 | polygonStateBridge.setFxChildTunnel(_fxChildTunnel); 191 | 192 | vm.expectRevert(FxBaseRootChildTunnelAlreadySet.selector); 193 | 194 | vm.prank(owner); 195 | polygonStateBridge.setFxChildTunnel(_fxChildTunnel); 196 | } 197 | 198 | /// @notice tests that the StateBridge contract's ownership can't be changed by a non-owner 199 | /// @param newOwner The new owner of the StateBridge contract (foundry fuzz) 200 | function test_notOwner_transferOwnership_reverts(address nonOwner, address newOwner) public { 201 | vm.assume(nonOwner != owner && nonOwner != address(0) && newOwner != address(0)); 202 | 203 | vm.expectRevert("Ownable: caller is not the owner"); 204 | 205 | vm.prank(nonOwner); 206 | polygonStateBridge.transferOwnership(newOwner); 207 | } 208 | 209 | /// @notice tests whether the StateBridge contract can set root history expiry on Optimism and Polygon 210 | /// @param _rootHistoryExpiry The new root history expiry for OpWorldID and PolygonWorldID 211 | function test_notOwner_setRootHistoryExpiryPolygon_reverts( 212 | address nonOwner, 213 | uint256 _rootHistoryExpiry 214 | ) public { 215 | vm.assume(nonOwner != owner && _rootHistoryExpiry != 0); 216 | 217 | vm.expectRevert("Ownable: caller is not the owner"); 218 | 219 | vm.prank(nonOwner); 220 | polygonStateBridge.setRootHistoryExpiryPolygon(_rootHistoryExpiry); 221 | } 222 | 223 | /// @notice Tests that a nonPendingOwner can't accept ownership of StateBridge 224 | /// @param newOwner the new owner of the contract 225 | function test_notOwner_acceptOwnership_reverts(address newOwner, address randomAddress) 226 | public 227 | { 228 | vm.assume( 229 | newOwner != address(0) && randomAddress != address(0) && randomAddress != newOwner 230 | ); 231 | 232 | vm.prank(owner); 233 | polygonStateBridge.transferOwnership(newOwner); 234 | 235 | vm.expectRevert("Ownable2Step: caller is not the new owner"); 236 | 237 | vm.prank(randomAddress); 238 | polygonStateBridge.acceptOwnership(); 239 | } 240 | 241 | /// @notice Tests that ownership can't be renounced 242 | function test_owner_renounceOwnership_reverts() public { 243 | vm.expectRevert(PolygonStateBridge.CannotRenounceOwnership.selector); 244 | 245 | vm.prank(owner); 246 | polygonStateBridge.renounceOwnership(); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/test/OpWorldID.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | /// @dev using Test from forge-std which is inherited from Optimism's CommonTest.t.sol 5 | // import { PRBTest } from "@prb/test/PRBTest.sol"; 6 | // import { StdCheats } from "forge-std/StdCheats.sol"; 7 | import {OpWorldID} from "src/OpWorldID.sol"; 8 | import {WorldIDBridge} from "src/abstract/WorldIDBridge.sol"; 9 | import {SemaphoreTreeDepthValidator} from "src/utils/SemaphoreTreeDepthValidator.sol"; 10 | import {L2CrossDomainMessenger} from 11 | "@eth-optimism/contracts-bedrock/contracts/L2/L2CrossDomainMessenger.sol"; 12 | import {Predeploys} from "@eth-optimism/contracts-bedrock/contracts/libraries/Predeploys.sol"; 13 | import { 14 | CommonTest, 15 | Messenger_Initializer 16 | } from "@eth-optimism/contracts-bedrock/contracts/test/CommonTest.t.sol"; 17 | import {AddressAliasHelper} from 18 | "@eth-optimism/contracts-bedrock/contracts/vendor/AddressAliasHelper.sol"; 19 | import {Encoding} from "@eth-optimism/contracts-bedrock/contracts/libraries/Encoding.sol"; 20 | import {Hashing} from "@eth-optimism/contracts-bedrock/contracts/libraries/Hashing.sol"; 21 | import {Bytes32AddressLib} from "solmate/src/utils/Bytes32AddressLib.sol"; 22 | 23 | /// @title OpWorldIDTest 24 | /// @author Worldcoin 25 | /// @notice A test contract for OpWorldID 26 | /// @dev The OpWorldID contract is deployed on Optimism and is called by the StateBridge contract. 27 | /// @dev This contract uses the Optimism CommonTest.t.sol testing tool suite. 28 | contract OpWorldIDTest is Messenger_Initializer { 29 | /////////////////////////////////////////////////////////////////// 30 | /// WORLD ID /// 31 | /////////////////////////////////////////////////////////////////// 32 | /// @notice The OpWorldID contract 33 | OpWorldID internal id; 34 | 35 | /// @notice MarkleTree depth 36 | uint8 internal treeDepth = 16; 37 | 38 | /// @notice OpenZeppelin Ownable.sol transferOwnership event 39 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 40 | 41 | /// @notice CrossDomainOwnable3.sol transferOwnership event 42 | event OwnershipTransferred( 43 | address indexed previousOwner, address indexed newOwner, bool isLocal 44 | ); 45 | 46 | function testConstructorWithInvalidTreeDepth(uint8 actualTreeDepth) public { 47 | // Setup 48 | vm.assume(!SemaphoreTreeDepthValidator.validate(actualTreeDepth)); 49 | vm.expectRevert(abi.encodeWithSignature("UnsupportedTreeDepth(uint8)", actualTreeDepth)); 50 | 51 | new OpWorldID(actualTreeDepth); 52 | } 53 | 54 | function setUp() public override { 55 | /// @notice CrossDomainOwnable3 setup 56 | super.setUp(); 57 | 58 | /// @notice Initialize the OpWorldID contract 59 | vm.prank(alice); 60 | id = new OpWorldID(treeDepth); 61 | 62 | /// @dev label important addresses 63 | vm.label(address(this), "Sender"); 64 | vm.label(address(id), "OPWorldID"); 65 | } 66 | 67 | /////////////////////////////////////////////////////////////////// 68 | /// SUCCEEDS /// 69 | /////////////////////////////////////////////////////////////////// 70 | 71 | function _switchToCrossDomainOwnership(OpWorldID _id) internal { 72 | vm.expectEmit(true, true, true, true); 73 | 74 | // OpenZeppelin Ownable.sol transferOwnership event 75 | emit OwnershipTransferred(alice, alice); 76 | 77 | // CrossDomainOwnable3.sol transferOwnership event 78 | emit OwnershipTransferred(alice, alice, false); 79 | 80 | // CrossDomainOwnable3.sol transferOwnership to crossDomain address (as alice and to alice) 81 | vm.prank(_id.owner()); 82 | id.transferOwnership({_owner: alice, _isLocal: false}); 83 | } 84 | 85 | /// @notice Test that you can insert new root and check if it is valid 86 | /// @param newRoot The root of the merkle tree after the first update 87 | function test_receiveVerifyRoot_succeeds(uint256 newRoot) public { 88 | vm.assume(newRoot != 0); 89 | 90 | _switchToCrossDomainOwnership(id); 91 | 92 | address owner = id.owner(); 93 | 94 | vm.warp(block.timestamp + 200); 95 | 96 | // set the xDomainMsgSender storage slot to the L1Messenger 97 | vm.prank(AddressAliasHelper.applyL1ToL2Alias(address(L1Messenger))); 98 | L2Messenger.relayMessage( 99 | Encoding.encodeVersionedNonce(0, 1), 100 | owner, 101 | address(id), 102 | 0, 103 | 0, 104 | abi.encodeWithSelector(id.receiveRoot.selector, newRoot) 105 | ); 106 | 107 | assert(id.latestRoot() == newRoot); 108 | } 109 | 110 | /// @notice Checks that it is possible to get the tree depth the contract was initialized with. 111 | function testCanGetTreeDepth(uint8 actualTreeDepth) public { 112 | // Setup 113 | vm.assume(SemaphoreTreeDepthValidator.validate(actualTreeDepth)); 114 | 115 | id = new OpWorldID(actualTreeDepth); 116 | 117 | // Test 118 | assert(id.getTreeDepth() == actualTreeDepth); 119 | } 120 | 121 | /////////////////////////////////////////////////////////////////// 122 | /// REVERTS /// 123 | /////////////////////////////////////////////////////////////////// 124 | 125 | /// @notice Test that when _isLocal = false, a contract that is not the L2 Messenger can't call the contract 126 | /// @param newRoot The root of the merkle tree after the first update 127 | function test_onlyOwner_notMessenger_reverts(uint256 newRoot) external { 128 | _switchToCrossDomainOwnership(id); 129 | 130 | // calling locally (not as the messenger) 131 | vm.prank(bob); 132 | vm.expectRevert("CrossDomainOwnable3: caller is not the messenger"); 133 | id.receiveRoot(newRoot); 134 | } 135 | 136 | /// @notice Test that a non-owner can't insert a new root 137 | /// @param newRoot The root of the merkle tree after the first update 138 | function test_onlyOwner_notOwner_reverts(uint256 newRoot) external { 139 | _switchToCrossDomainOwnership(id); 140 | 141 | // set the xDomainMsgSender storage slot as bob 142 | bytes32 key = bytes32(uint256(204)); 143 | bytes32 value = Bytes32AddressLib.fillLast12Bytes(address(bob)); 144 | vm.store(address(L2Messenger), key, value); 145 | 146 | vm.prank(address(L2Messenger)); 147 | vm.expectRevert("CrossDomainOwnable3: caller is not the owner"); 148 | id.receiveRoot(newRoot); 149 | } 150 | 151 | /// @notice Test that a root that hasn't been inserted is invalid 152 | /// @param newRoot The root of the merkle tree after the first update 153 | function test_receiveVerifyInvalidRoot_reverts(uint256 newRoot, uint256[8] memory proof) 154 | public 155 | { 156 | _switchToCrossDomainOwnership(id); 157 | 158 | address owner = id.owner(); 159 | 160 | vm.warp(block.timestamp + 200); 161 | uint256 randomRoot = 0x712cab3414951eba341ca234aef42142567c6eea50371dd528d57eb2b856d238; 162 | 163 | // set the xDomainMsgSender storage slot to the L1Messenger 164 | vm.prank(AddressAliasHelper.applyL1ToL2Alias(address(L1Messenger))); 165 | L2Messenger.relayMessage( 166 | Encoding.encodeVersionedNonce(0, 1), 167 | owner, 168 | address(id), 169 | 0, 170 | 0, 171 | abi.encodeWithSelector(id.receiveRoot.selector, newRoot) 172 | ); 173 | 174 | vm.expectRevert(WorldIDBridge.NonExistentRoot.selector); 175 | id.verifyProof(randomRoot, 0, 0, 0, proof); 176 | } 177 | 178 | /// @notice Test that you can insert a root and check it has expired if more than 7 days have passed 179 | /// @param newRoot The root of the merkle tree after the first update (forge fuzzing) 180 | /// @param secondRoot The root of the merkle tree after the second update (forge fuzzing) 181 | function test_expiredRoot_reverts(uint256 newRoot, uint256 secondRoot, uint256[8] memory proof) 182 | public 183 | { 184 | vm.assume(newRoot != secondRoot && newRoot != 0 && secondRoot != 0); 185 | 186 | _switchToCrossDomainOwnership(id); 187 | 188 | address owner = id.owner(); 189 | 190 | // set the xDomainMsgSender storage slot to the L1Messenger 191 | vm.prank(AddressAliasHelper.applyL1ToL2Alias(address(L1Messenger))); 192 | L2Messenger.relayMessage( 193 | Encoding.encodeVersionedNonce(0, 1), 194 | owner, 195 | address(id), 196 | 0, 197 | 0, 198 | abi.encodeWithSelector(id.receiveRoot.selector, newRoot) 199 | ); 200 | 201 | vm.roll(block.number + 100); 202 | vm.warp(block.timestamp + 200); 203 | vm.prank(AddressAliasHelper.applyL1ToL2Alias(address(L1Messenger))); 204 | L2Messenger.relayMessage( 205 | Encoding.encodeVersionedNonce(1, 1), 206 | owner, 207 | address(id), 208 | 0, 209 | 0, 210 | abi.encodeWithSelector(id.receiveRoot.selector, secondRoot) 211 | ); 212 | 213 | vm.expectRevert(WorldIDBridge.ExpiredRoot.selector); 214 | vm.warp(block.timestamp + 8 days); 215 | id.verifyProof(newRoot, 0, 0, 0, proof); 216 | } 217 | 218 | function test_receiveRoot_reverts_CannotOverwriteRoot(uint256 newRoot) public { 219 | vm.assume(newRoot != 0); 220 | 221 | _switchToCrossDomainOwnership(id); 222 | 223 | address owner = id.owner(); 224 | 225 | // set the xDomainMsgSender storage slot to the L1Messenger 226 | vm.prank(AddressAliasHelper.applyL1ToL2Alias(address(L1Messenger))); 227 | L2Messenger.relayMessage( 228 | Encoding.encodeVersionedNonce(0, 1), 229 | owner, 230 | address(id), 231 | 0, 232 | 0, 233 | abi.encodeWithSelector(id.receiveRoot.selector, newRoot) 234 | ); 235 | 236 | assert(id.latestRoot() == newRoot); 237 | 238 | bytes32 versionedHash = Hashing.hashCrossDomainMessageV1( 239 | Encoding.encodeVersionedNonce(1, 1), 240 | owner, 241 | address(id), 242 | 0, 243 | 0, 244 | abi.encodeWithSelector(id.receiveRoot.selector, newRoot) 245 | ); 246 | 247 | // It reverts with CannotOverwriteRoot however because of the bridge simulation 248 | // the L2 cross-domain call doesn't revert, however it does emit a FailedRelayedMessage 249 | // issue reported to foundry team: expectRevert doesn't search for errors in nested subcalls 250 | // vm.expectRevert(abi.encodeWithSelector(WorldIDBridge.CannotOverwriteRoot.selector)); 251 | // CannotOverwriteRoot can be seen in the execution trace of the call using the -vvvvv flag 252 | 253 | vm.expectEmit(true, true, true, true); 254 | emit FailedRelayedMessage(versionedHash); 255 | 256 | vm.roll(block.number + 100); 257 | vm.warp(block.timestamp + 200); 258 | // set the xDomainMsgSender storage slot to the L1Messenger 259 | vm.prank(AddressAliasHelper.applyL1ToL2Alias(address(L1Messenger))); 260 | L2Messenger.relayMessage( 261 | Encoding.encodeVersionedNonce(1, 1), 262 | owner, 263 | address(id), 264 | 0, 265 | 0, 266 | abi.encodeWithSelector(id.receiveRoot.selector, newRoot) 267 | ); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/OpStateBridge.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | // Optimism interface for cross domain messaging 5 | import {ICrossDomainMessenger} from 6 | "@eth-optimism/contracts/libraries/bridge/ICrossDomainMessenger.sol"; 7 | import {IOpWorldID} from "./interfaces/IOpWorldID.sol"; 8 | import {IRootHistory} from "./interfaces/IRootHistory.sol"; 9 | import {IWorldIDIdentityManager} from "./interfaces/IWorldIDIdentityManager.sol"; 10 | import {Ownable2Step} from "openzeppelin-contracts/access/Ownable2Step.sol"; 11 | import {ICrossDomainOwnable3} from "./interfaces/ICrossDomainOwnable3.sol"; 12 | 13 | /// @title World ID State Bridge Optimism 14 | /// @author Worldcoin 15 | /// @notice Distributes new World ID Identity Manager roots to an OP Stack network 16 | /// @dev This contract lives on Ethereum mainnet and works for Optimism and any OP Stack based chain 17 | contract OpStateBridge is Ownable2Step { 18 | /////////////////////////////////////////////////////////////////// 19 | /// STORAGE /// 20 | /////////////////////////////////////////////////////////////////// 21 | 22 | /// @notice The address of the OpWorldID contract on any OP Stack chain 23 | address public immutable opWorldIDAddress; 24 | 25 | /// @notice address for OP Stack chain Ethereum mainnet L1CrossDomainMessenger contract 26 | address internal immutable crossDomainMessengerAddress; 27 | 28 | /// @notice Ethereum mainnet worldID Address 29 | address public immutable worldIDAddress; 30 | 31 | /// @notice Amount of gas purchased on the OP Stack chain for propagateRoot 32 | uint32 internal _gasLimitPropagateRoot; 33 | 34 | /// @notice Amount of gas purchased on the OP Stack chain for SetRootHistoryExpiry 35 | uint32 internal _gasLimitSetRootHistoryExpiry; 36 | 37 | /// @notice Amount of gas purchased on the OP Stack chain for transferOwnershipOp 38 | uint32 internal _gasLimitTransferOwnership; 39 | 40 | /// @notice The default gas limit amount to buy on an OP stack chain to do simple transactions 41 | uint32 public constant DEFAULT_OP_GAS_LIMIT = 1000000; 42 | 43 | /////////////////////////////////////////////////////////////////// 44 | /// EVENTS /// 45 | /////////////////////////////////////////////////////////////////// 46 | 47 | /// @notice Emitted when the StateBridge gives ownership of the OPWorldID contract 48 | /// to the WorldID Identity Manager contract away 49 | /// @param previousOwner The previous owner of the OPWorldID contract 50 | /// @param newOwner The new owner of the OPWorldID contract 51 | /// @param isLocal Whether the ownership transfer is local (Optimism/OP Stack chain EOA/contract) 52 | /// or an Ethereum EOA or contract 53 | event OwnershipTransferredOp( 54 | address indexed previousOwner, address indexed newOwner, bool isLocal 55 | ); 56 | 57 | /// @notice Emitted when the StateBridge sends a root to the OPWorldID contract 58 | /// @param root The root sent to the OPWorldID contract on the OP Stack chain 59 | event RootPropagated(uint256 root); 60 | 61 | /// @notice Emitted when the StateBridge sets the root history expiry for OpWorldID and PolygonWorldID 62 | /// @param rootHistoryExpiry The new root history expiry 63 | event SetRootHistoryExpiry(uint256 rootHistoryExpiry); 64 | 65 | /// @notice Emitted when the StateBridge sets the gas limit for sendRootOp 66 | /// @param _opGasLimit The new opGasLimit for sendRootOp 67 | event SetGasLimitPropagateRoot(uint32 _opGasLimit); 68 | 69 | /// @notice Emitted when the StateBridge sets the gas limit for SetRootHistoryExpiry 70 | /// @param _opGasLimit The new opGasLimit for SetRootHistoryExpiry 71 | event SetGasLimitSetRootHistoryExpiry(uint32 _opGasLimit); 72 | 73 | /// @notice Emitted when the StateBridge sets the gas limit for transferOwnershipOp 74 | /// @param _opGasLimit The new opGasLimit for transferOwnershipOptimism 75 | event SetGasLimitTransferOwnershipOp(uint32 _opGasLimit); 76 | 77 | /////////////////////////////////////////////////////////////////// 78 | /// ERRORS /// 79 | /////////////////////////////////////////////////////////////////// 80 | 81 | /// @notice Emitted when an attempt is made to renounce ownership. 82 | error CannotRenounceOwnership(); 83 | 84 | /// @notice Emitted when an attempt is made to set the gas limit to zero 85 | error GasLimitZero(); 86 | 87 | /// @notice Emitted when an attempt is made to set an address to zero 88 | error AddressZero(); 89 | 90 | /////////////////////////////////////////////////////////////////// 91 | /// CONSTRUCTOR /// 92 | /////////////////////////////////////////////////////////////////// 93 | 94 | /// @notice constructor 95 | /// @param _worldIDIdentityManager Deployment address of the WorldID Identity Manager contract 96 | /// @param _opWorldIDAddress Address of the Optimism contract that will receive the new root and timestamp 97 | /// @param _crossDomainMessenger L1CrossDomainMessenger contract used to communicate with the desired OP 98 | /// Stack network 99 | /// @custom:revert if any of the constructor params addresses are zero 100 | constructor( 101 | address _worldIDIdentityManager, 102 | address _opWorldIDAddress, 103 | address _crossDomainMessenger 104 | ) { 105 | if ( 106 | _worldIDIdentityManager == address(0) || _opWorldIDAddress == address(0) 107 | || _crossDomainMessenger == address(0) 108 | ) { 109 | revert AddressZero(); 110 | } 111 | 112 | opWorldIDAddress = _opWorldIDAddress; 113 | worldIDAddress = _worldIDIdentityManager; 114 | crossDomainMessengerAddress = _crossDomainMessenger; 115 | _gasLimitPropagateRoot = DEFAULT_OP_GAS_LIMIT; 116 | _gasLimitSetRootHistoryExpiry = DEFAULT_OP_GAS_LIMIT; 117 | _gasLimitTransferOwnership = DEFAULT_OP_GAS_LIMIT; 118 | } 119 | 120 | /////////////////////////////////////////////////////////////////// 121 | /// PUBLIC API /// 122 | /////////////////////////////////////////////////////////////////// 123 | 124 | /// @notice Sends the latest WorldID Identity Manager root to the IOpStack. 125 | /// @dev Calls this method on the L1 Proxy contract to relay roots to the destination OP Stack chain 126 | function propagateRoot() external { 127 | uint256 latestRoot = IWorldIDIdentityManager(worldIDAddress).latestRoot(); 128 | 129 | // The `encodeCall` function is strongly typed, so this checks that we are passing the 130 | // correct data to the optimism bridge. 131 | bytes memory message = abi.encodeCall(IOpWorldID.receiveRoot, (latestRoot)); 132 | 133 | ICrossDomainMessenger(crossDomainMessengerAddress).sendMessage( 134 | // Contract address on the OP Stack Chain 135 | opWorldIDAddress, 136 | message, 137 | _gasLimitPropagateRoot 138 | ); 139 | 140 | emit RootPropagated(latestRoot); 141 | } 142 | 143 | /// @notice Adds functionality to the StateBridge to transfer ownership 144 | /// of OpWorldID to another contract on L1 or to a local OP Stack chain EOA 145 | /// @param _owner new owner (EOA or contract) 146 | /// @param _isLocal true if new owner is on Optimism, false if it is a cross-domain owner 147 | /// @custom:revert if _owner is set to the zero address 148 | function transferOwnershipOp(address _owner, bool _isLocal) external onlyOwner { 149 | if (_owner == address(0)) { 150 | revert AddressZero(); 151 | } 152 | 153 | // The `encodeCall` function is strongly typed, so this checks that we are passing the 154 | // correct data to the OP Stack chain bridge. 155 | bytes memory message = 156 | abi.encodeCall(ICrossDomainOwnable3.transferOwnership, (_owner, _isLocal)); 157 | 158 | ICrossDomainMessenger(crossDomainMessengerAddress).sendMessage( 159 | // Contract address on the OP Stack Chain 160 | opWorldIDAddress, 161 | message, 162 | _gasLimitTransferOwnership 163 | ); 164 | 165 | emit OwnershipTransferredOp(owner(), _owner, _isLocal); 166 | } 167 | 168 | /// @notice Adds functionality to the StateBridge to set the root history expiry on OpWorldID 169 | /// @param _rootHistoryExpiry new root history expiry 170 | function setRootHistoryExpiry(uint256 _rootHistoryExpiry) external onlyOwner { 171 | // The `encodeCall` function is strongly typed, so this checks that we are passing the 172 | // correct data to the optimism bridge. 173 | bytes memory message = 174 | abi.encodeCall(IRootHistory.setRootHistoryExpiry, (_rootHistoryExpiry)); 175 | 176 | ICrossDomainMessenger(crossDomainMessengerAddress).sendMessage( 177 | // Contract address on the OP Stack Chain 178 | opWorldIDAddress, 179 | message, 180 | _gasLimitSetRootHistoryExpiry 181 | ); 182 | 183 | emit SetRootHistoryExpiry(_rootHistoryExpiry); 184 | } 185 | 186 | /////////////////////////////////////////////////////////////////// 187 | /// OP GAS LIMIT /// 188 | /////////////////////////////////////////////////////////////////// 189 | 190 | /// @notice Sets the gas limit for the propagateRoot method 191 | /// @param _opGasLimit The new gas limit for the propagateRoot method 192 | function setGasLimitPropagateRoot(uint32 _opGasLimit) external onlyOwner { 193 | if (_opGasLimit <= 0) { 194 | revert GasLimitZero(); 195 | } 196 | 197 | _gasLimitPropagateRoot = _opGasLimit; 198 | 199 | emit SetGasLimitPropagateRoot(_opGasLimit); 200 | } 201 | 202 | /// @notice Sets the gas limit for the SetRootHistoryExpiry method 203 | /// @param _opGasLimit The new gas limit for the SetRootHistoryExpiry method 204 | function setGasLimitSetRootHistoryExpiry(uint32 _opGasLimit) external onlyOwner { 205 | if (_opGasLimit <= 0) { 206 | revert GasLimitZero(); 207 | } 208 | 209 | _gasLimitSetRootHistoryExpiry = _opGasLimit; 210 | 211 | emit SetGasLimitSetRootHistoryExpiry(_opGasLimit); 212 | } 213 | 214 | /// @notice Sets the gas limit for the transferOwnershipOp method 215 | /// @param _opGasLimit The new gas limit for the transferOwnershipOp method 216 | function setGasLimitTransferOwnershipOp(uint32 _opGasLimit) external onlyOwner { 217 | if (_opGasLimit <= 0) { 218 | revert GasLimitZero(); 219 | } 220 | 221 | _gasLimitTransferOwnership = _opGasLimit; 222 | 223 | emit SetGasLimitTransferOwnershipOp(_opGasLimit); 224 | } 225 | 226 | /////////////////////////////////////////////////////////////////// 227 | /// OWNERSHIP /// 228 | /////////////////////////////////////////////////////////////////// 229 | /// @notice Ensures that ownership of WorldID implementations cannot be renounced. 230 | /// @dev This function is intentionally not `virtual` as we do not want it to be possible to 231 | /// renounce ownership for any WorldID implementation. 232 | /// @dev This function is marked as `onlyOwner` to maintain the access restriction from the base 233 | /// contract. 234 | function renounceOwnership() public view override onlyOwner { 235 | revert CannotRenounceOwnership(); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/abstract/WorldIDBridge.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.15; 3 | 4 | import {IWorldID} from "../interfaces/IWorldID.sol"; 5 | 6 | import {SemaphoreTreeDepthValidator} from "../utils/SemaphoreTreeDepthValidator.sol"; 7 | import {SemaphoreVerifier} from "src/SemaphoreVerifier.sol"; 8 | 9 | /// @title Bridged World ID 10 | /// @author Worldcoin 11 | /// @notice A base contract for the WorldID state bridges that exist on other chains. The state 12 | /// bridges manage the root history of the identity merkle tree on chains other than 13 | /// mainnet. 14 | /// @dev This contract abstracts the common functionality, allowing for easier understanding and 15 | /// code reuse. 16 | /// @dev This contract is very explicitly not able to be instantiated. Do not un-mark it as 17 | /// `abstract`. 18 | abstract contract WorldIDBridge is IWorldID { 19 | /////////////////////////////////////////////////////////////////////////////// 20 | /// CONTRACT DATA /// 21 | /////////////////////////////////////////////////////////////////////////////// 22 | 23 | /// @notice The depth of the merkle tree used to store identities. 24 | uint8 internal immutable treeDepth; 25 | 26 | /// @notice The amount of time a root is considered as valid on the bridged chain. 27 | uint256 internal ROOT_HISTORY_EXPIRY = 1 weeks; 28 | 29 | /// @notice The value of the latest merkle tree root. 30 | uint256 internal _latestRoot; 31 | 32 | /// @notice The mapping between the value of the merkle tree root and the timestamp at which it 33 | /// entered the root history. 34 | mapping(uint256 => uint128) public rootHistory; 35 | 36 | /// @notice The time in the `rootHistory` mapping associated with a root that has never been 37 | /// seen before. 38 | uint128 internal constant NULL_ROOT_TIME = 0; 39 | 40 | /// @notice The verifier instance needed to operate within the semaphore protocol. 41 | SemaphoreVerifier internal semaphoreVerifier = new SemaphoreVerifier(); 42 | 43 | /////////////////////////////////////////////////////////////////////////////// 44 | /// ERRORS /// 45 | /////////////////////////////////////////////////////////////////////////////// 46 | 47 | /// @notice Emitted when the provided semaphore tree depth is unsupported. 48 | /// 49 | /// @param depth The tree depth that was passed. 50 | error UnsupportedTreeDepth(uint8 depth); 51 | 52 | /// @notice Emitted when attempting to validate a root that has expired. 53 | error ExpiredRoot(); 54 | 55 | /// @notice Emitted when attempting to validate a root that has yet to be added to the root 56 | /// history. 57 | error NonExistentRoot(); 58 | 59 | /// @notice Emitted when attempting to update the timestamp for a root that already has one. 60 | error CannotOverwriteRoot(); 61 | 62 | /// @notice Emitted if the latest root is requested but the bridge has not seen any roots yet. 63 | error NoRootsSeen(); 64 | 65 | /////////////////////////////////////////////////////////////////////////////// 66 | /// EVENTS /// 67 | /////////////////////////////////////////////////////////////////////////////// 68 | 69 | /// @notice Emitted when a new root is received by the contract. 70 | /// 71 | /// @param root The value of the root that was added. 72 | /// @param timestamp The timestamp of insertion for the given root. 73 | event RootAdded(uint256 root, uint128 timestamp); 74 | 75 | /// @notice Emitted when the expiry time for the root history is updated. 76 | /// 77 | /// @param newExpiry The new expiry time. 78 | event RootHistoryExpirySet(uint256 newExpiry); 79 | 80 | /////////////////////////////////////////////////////////////////////////////// 81 | /// CONSTRUCTION /// 82 | /////////////////////////////////////////////////////////////////////////////// 83 | 84 | /// @notice Constructs a new instance of the state bridge. 85 | /// 86 | /// @param _treeDepth The depth of the identities merkle tree. 87 | constructor(uint8 _treeDepth) { 88 | if (!SemaphoreTreeDepthValidator.validate(_treeDepth)) { 89 | revert UnsupportedTreeDepth(_treeDepth); 90 | } 91 | 92 | treeDepth = _treeDepth; 93 | } 94 | 95 | /////////////////////////////////////////////////////////////////////////////// 96 | /// ROOT MIRRORING /// 97 | /////////////////////////////////////////////////////////////////////////////// 98 | 99 | /// @notice This function is called by the state bridge contract when it forwards a new root to 100 | /// the bridged WorldID. 101 | /// @dev Intended to be called from a privilege-checked implementation of `receiveRoot` or an 102 | /// equivalent operation. 103 | /// 104 | /// @param newRoot The value of the new root. 105 | /// 106 | /// @custom:reverts CannotOverwriteRoot If the root already exists in the root history. 107 | function _receiveRoot(uint256 newRoot) internal { 108 | uint256 existingTimestamp = rootHistory[newRoot]; 109 | 110 | if (existingTimestamp != NULL_ROOT_TIME) { 111 | revert CannotOverwriteRoot(); 112 | } 113 | 114 | uint128 currTimestamp = uint128(block.timestamp); 115 | 116 | _latestRoot = newRoot; 117 | rootHistory[newRoot] = currTimestamp; 118 | 119 | emit RootAdded(newRoot, currTimestamp); 120 | } 121 | 122 | /// @notice Reverts if the provided root value is not valid. 123 | /// @dev A root is valid if it is either the latest root, or not the latest root but has not 124 | /// expired. 125 | /// 126 | /// @param root The root of the merkle tree to check for validity. 127 | /// 128 | /// @custom:reverts ExpiredRoot If the provided `root` has expired. 129 | /// @custom:reverts NonExistentRoot If the provided `root` does not exist in the history. 130 | function requireValidRoot(uint256 root) internal view { 131 | // The latest root is always valid. 132 | if (root == _latestRoot) { 133 | return; 134 | } 135 | 136 | // Otherwise, we need to check things via the timestamp. 137 | uint128 rootTimestamp = rootHistory[root]; 138 | 139 | // A root does not exist if it has no associated timestamp. 140 | if (rootTimestamp == 0) { 141 | revert NonExistentRoot(); 142 | } 143 | 144 | // A root is no longer valid if it has expired. 145 | if (block.timestamp - rootTimestamp > ROOT_HISTORY_EXPIRY) { 146 | revert ExpiredRoot(); 147 | } 148 | } 149 | 150 | /////////////////////////////////////////////////////////////////////////////// 151 | /// SEMAPHORE PROOFS /// 152 | /////////////////////////////////////////////////////////////////////////////// 153 | 154 | /// @notice A verifier for the semaphore protocol that supports compressed proofs and is backwards 155 | /// compatible with the previous implementation. If a proof is compressed, the last 4 uints 156 | /// will be 0. If this condition is met, we will call the semaphore compress proof function 157 | /// instead. 158 | /// @dev Note that a double-signaling check is not included here, and should be carried by the 159 | /// caller. 160 | /// 161 | /// @param root The root of the Merkle tree 162 | /// @param signalHash A keccak256 hash of the Semaphore signal 163 | /// @param nullifierHash The nullifier hash 164 | /// @param externalNullifierHash A keccak256 hash of the external nullifier 165 | /// @param proof The zero-knowledge proof 166 | /// @custom:reverts string If the zero-knowledge proof cannot be verified for the public inputs. 167 | function verifyProof( 168 | uint256 root, 169 | uint256 signalHash, 170 | uint256 nullifierHash, 171 | uint256 externalNullifierHash, 172 | uint256[8] calldata proof 173 | ) public view virtual { 174 | // Check the preconditions on the inputs. 175 | requireValidRoot(root); 176 | uint256[4] memory input = [root, nullifierHash, signalHash, externalNullifierHash]; 177 | if (proof[4] == 0 && proof[5] == 0 && proof[6] == 0 && proof[7] == 0) { 178 | uint256[4] memory compressedProof = [proof[0], proof[1], proof[2], proof[3]]; 179 | semaphoreVerifier.verifyCompressedProof(compressedProof, input); 180 | } else { 181 | semaphoreVerifier.verifyProof(proof, input); 182 | } 183 | } 184 | 185 | /// @notice A verifier for the compressed semaphore protocol. 186 | /// @dev Note that a double-signaling check is not included here, and should be carried by the 187 | /// caller. 188 | /// 189 | /// @param root The root of the Merkle tree 190 | /// @param signalHash A keccak256 hash of the Semaphore signal 191 | /// @param nullifierHash The nullifier hash 192 | /// @param externalNullifierHash A keccak256 hash of the external nullifier 193 | /// @param compressedProof The compressed zero-knowledge proof 194 | /// 195 | /// @custom:reverts string If the zero-knowledge proof cannot be verified for the public inputs. 196 | function verifyCompressedProof( 197 | uint256 root, 198 | uint256 signalHash, 199 | uint256 nullifierHash, 200 | uint256 externalNullifierHash, 201 | uint256[4] calldata compressedProof 202 | ) public view virtual { 203 | // Check the preconditions on the inputs. 204 | requireValidRoot(root); 205 | 206 | // With that done we can now verify the proof. 207 | semaphoreVerifier.verifyCompressedProof( 208 | compressedProof, [root, nullifierHash, signalHash, externalNullifierHash] 209 | ); 210 | } 211 | 212 | /////////////////////////////////////////////////////////////////////////////// 213 | /// DATA MANAGEMENT /// 214 | /////////////////////////////////////////////////////////////////////////////// 215 | 216 | /// @notice Gets the value of the latest root. 217 | /// 218 | /// @custom:reverts NoRootsSeen If there is no latest root. 219 | function latestRoot() public view virtual returns (uint256) { 220 | if (_latestRoot == 0) { 221 | revert NoRootsSeen(); 222 | } 223 | 224 | return _latestRoot; 225 | } 226 | 227 | /// @notice Gets the amount of time it takes for a root in the root history to expire. 228 | function rootHistoryExpiry() public view virtual returns (uint256) { 229 | return ROOT_HISTORY_EXPIRY; 230 | } 231 | 232 | /// @notice Sets the amount of time it takes for a root in the root history to expire. 233 | /// @dev When implementing this function, ensure that it is guarded on `onlyOwner`. 234 | /// 235 | /// @param expiryTime The new amount of time it takes for a root to expire. 236 | function setRootHistoryExpiry(uint256 expiryTime) public virtual; 237 | 238 | /// @notice Sets the amount of time it takes for a root in the root history to expire. 239 | /// @dev Intended to be called from a privilege-checked implementation of `receiveRoot`. 240 | /// 241 | /// @param expiryTime The new amount of time it takes for a root to expire. 242 | function _setRootHistoryExpiry(uint256 expiryTime) internal virtual { 243 | ROOT_HISTORY_EXPIRY = expiryTime; 244 | 245 | emit RootHistoryExpirySet(expiryTime); 246 | } 247 | 248 | /// @notice Gets the Semaphore tree depth the contract was initialized with. 249 | function getTreeDepth() public view virtual returns (uint8) { 250 | return treeDepth; 251 | } 252 | } 253 | --------------------------------------------------------------------------------