59 | )
60 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # World ID On-Chain Template
2 |
3 | Template repository for a World ID On-Chain Integration.
4 |
5 | ## Local Development
6 |
7 | ### Prerequisites
8 |
9 | Create a staging on-chain app in the [Worldcoin Developer Portal](https://developer.worldcoin.org).
10 |
11 | Ensure you have installed [Foundry](https://book.getfoundry.sh/getting-started/installation), [NodeJS](https://nodejs.org/en/download), and [pnpm](https://pnpm.io/installation).
12 |
13 | ### Local Testnet Setup
14 |
15 | Start a local node forked from Optimism Sepolia, replacing `$YOUR_API_KEY` with your Alchemy API key:
16 |
17 | ```bash
18 | # leave this running in the background
19 | anvil -f https://opt-sepolia.g.alchemy.com/v2/$YOUR_API_KEY
20 | ```
21 |
22 | In another shell, deploy the contract, replacing `$WORLD_ID_ROUTER` with the [World ID Router address](https://docs.worldcoin.org/reference/address-book) for your selected chain, `$NEXT_PUBLIC_APP_ID` with the app ID as configured in the [Worldcoin Developer Portal](https://developer.worldcoin.org), and `$NEXT_PUBLIC_ACTION` with the action ID as configured in the Worldcoin Developer Portal:
23 |
24 | ```bash
25 | cd contracts
26 | forge create --rpc-url http://localhost:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 src/Contract.sol:Contract --constructor-args $WORLD_ID_ROUTER $NEXT_PUBLIC_APP_ID $NEXT_PUBLIC_ACTION
27 | ```
28 |
29 | Note the `Deployed to:` address from the output.
30 |
31 | ### Local Web Setup
32 |
33 | In a new shell, install project dependencies:
34 |
35 | ```bash
36 | pnpm i
37 | ```
38 |
39 | Set up your environment variables in the `.env` file. You will need to set the following variables:
40 | - `NEXT_PUBLIC_APP_ID`: The app ID as configured in the [Worldcoin Developer Portal](https://developer.worldcoin.org).
41 | - `NEXT_PUBLIC_ACTION`: The action ID as configured in the Worldcoin Developer Portal.
42 | - `NEXT_PUBLIC_WALLETCONNECT_ID`: Your WalletConnect ID.
43 | - `NEXT_PUBLIC_CONTRACT_ADDRESS`: The address of the contract deployed in the previous step.
44 |
45 | Start the development server:
46 |
47 | ```bash
48 | pnpm dev
49 | ```
50 |
51 | The Contract ABI will be automatically re-generated and saved to `src/abi/ContractAbi.json` on each run of `pnpm dev`.
52 |
53 | ### Iterating
54 |
55 | After making changes to the contract, you should:
56 | - re-run the `forge create` command from above
57 | - replace the `NEXT_PUBLIC_CONTRACT_ADDRESS` environment variable with the new contract address
58 | - if your contract ABI has changed, restart the local web server
59 |
60 | ### Testing
61 |
62 | You'll need to import the private keys on the local testnet into your wallet used for local development. The default development seed phrase is `test test test test test test test test test test test junk`.
63 |
64 | > [!CAUTION]
65 | > This is only for local development. Do not use this seed phrase on mainnet or any public testnet.
66 |
67 | When connecting your wallet to the local development environment, you will be prompted to add the network to your wallet.
68 |
69 | Use the [Worldcoin Simulator](https://simulator.worldcoin.org) in place of World App to scan the IDKit QR codes and generate the zero-knowledge proofs.
--------------------------------------------------------------------------------
/contracts/src/Contract.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.13;
3 |
4 | import { ByteHasher } from './helpers/ByteHasher.sol';
5 | import { IWorldID } from './interfaces/IWorldID.sol';
6 |
7 | contract Contract {
8 | using ByteHasher for bytes;
9 |
10 | ///////////////////////////////////////////////////////////////////////////////
11 | /// ERRORS ///
12 | //////////////////////////////////////////////////////////////////////////////
13 |
14 | /// @notice Thrown when attempting to reuse a nullifier
15 | error DuplicateNullifier(uint256 nullifierHash);
16 |
17 | /// @dev The World ID instance that will be used for verifying proofs
18 | IWorldID internal immutable worldId;
19 |
20 | /// @dev The contract's external nullifier hash
21 | uint256 internal immutable externalNullifier;
22 |
23 | /// @dev The World ID group ID (always 1)
24 | uint256 internal immutable groupId = 1;
25 |
26 | /// @dev Whether a nullifier hash has been used already. Used to guarantee an action is only performed once by a single person
27 | mapping(uint256 => bool) internal nullifierHashes;
28 |
29 | /// @param nullifierHash The nullifier hash for the verified proof
30 | /// @dev A placeholder event that is emitted when a user successfully verifies with World ID
31 | event Verified(uint256 nullifierHash);
32 |
33 | /// @param _worldId The WorldID router that will verify the proofs
34 | /// @param _appId The World ID app ID
35 | /// @param _actionId The World ID action ID
36 | constructor(IWorldID _worldId, string memory _appId, string memory _actionId) {
37 | worldId = _worldId;
38 | externalNullifier = abi.encodePacked(abi.encodePacked(_appId).hashToField(), _actionId).hashToField();
39 | }
40 |
41 | /// @param signal An arbitrary input from the user, usually the user's wallet address (check README for further details)
42 | /// @param root The root of the Merkle tree (returned by the JS widget).
43 | /// @param nullifierHash The nullifier hash for this proof, preventing double signaling (returned by the JS widget).
44 | /// @param proof The zero-knowledge proof that demonstrates the claimer is registered with World ID (returned by the JS widget).
45 | /// @dev Feel free to rename this method however you want! We've used `claim`, `verify` or `execute` in the past.
46 | function verifyAndExecute(address signal, uint256 root, uint256 nullifierHash, uint256[8] calldata proof) public {
47 | // First, we make sure this person hasn't done this before
48 | if (nullifierHashes[nullifierHash]) revert DuplicateNullifier(nullifierHash);
49 |
50 | // We now verify the provided proof is valid and the user is verified by World ID
51 | worldId.verifyProof(
52 | root,
53 | groupId,
54 | abi.encodePacked(signal).hashToField(),
55 | nullifierHash,
56 | externalNullifier,
57 | proof
58 | );
59 |
60 | // We now record the user has done this, so they can't do it again (proof of uniqueness)
61 | nullifierHashes[nullifierHash] = true;
62 |
63 | // Finally, execute your logic here, for example issue a token, NFT, etc...
64 | // Make sure to emit some kind of event afterwards!
65 |
66 | emit Verified(nullifierHash);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------