Building on World Chain
Deploy a World ID template app
In this tutorial we are going to deploy a World ID template app on a World Chain Sepolia local fork using anvil.
This app will be a simple web application that allows users to create a World ID proof of personhood and verify it.
The app will be deployed on World Chain Sepolia and will interact with the WorldIDRouter
smart contract
to verify the ZK proofs of personhood.
Prerequisites
Before we start, make sure you have the following tools installed:
- Git (usually pre-installed on most systems)
- Node.js
- pnpm (or equivalent like npm, yarn or bun)
- Foundry CLI
Clone template
First, clone the World ID template app repository from GitHub:
git clone https://github.com/worldcoin/world-id-onchain-template.git && cd world-id-onchain-template
Install dependencies
Next, install the dependencies for the World ID template app:
pnpm install
Build the smart contracts
Next we are going to compile the smart contracts for the World ID template app:
cd contracts && forge build
Understanding World ID
Before we deploy the World ID template app, let's take a look at the smart contracts that are part of the app:
contracts/src/Contract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import { ByteHasher } from './helpers/ByteHasher.sol';
import { IWorldID } from './interfaces/IWorldID.sol';
contract Contract {
using ByteHasher for bytes;
///////////////////////////////////////////////////////////////////////////////
/// ERRORS ///
//////////////////////////////////////////////////////////////////////////////
/// @notice Thrown when attempting to reuse a nullifier
error DuplicateNullifier(uint256 nullifierHash);
/// @dev The World ID instance that will be used for verifying proofs
IWorldID internal immutable worldId;
/// @dev The contract's external nullifier hash
uint256 internal immutable externalNullifier;
/// @dev The World ID group ID (always 1)
uint256 internal immutable groupId = 1;
/// @dev Whether a nullifier hash has been used already. Used to guarantee an action is only performed once by a single person
mapping(uint256 => bool) internal nullifierHashes;
/// @param nullifierHash The nullifier hash for the verified proof
/// @dev A placeholder event that is emitted when a user successfully verifies with World ID
event Verified(uint256 nullifierHash);
/// @param _worldId The WorldID router that will verify the proofs
/// @param _appId The World ID app ID
/// @param _actionId The World ID action ID
constructor(IWorldID _worldId, string memory _appId, string memory _actionId) {
worldId = _worldId;
externalNullifier = abi.encodePacked(abi.encodePacked(_appId).hashToField(), _actionId).hashToField();
}
/// @param signal An arbitrary input from the user, usually the user's wallet address (check README for further details)
/// @param root The root of the Merkle tree (returned by the JS widget).
/// @param nullifierHash The nullifier hash for this proof, preventing double signaling (returned by the JS widget).
/// @param proof The zero-knowledge proof that demonstrates the claimer is registered with World ID (returned by the JS widget).
/// @dev Feel free to rename this method however you want! We've used `claim`, `verify` or `execute` in the past.
function verifyAndExecute(address signal, uint256 root, uint256 nullifierHash, uint256[8] calldata proof) public {
// First, we make sure this person hasn't done this before
if (nullifierHashes[nullifierHash]) revert DuplicateNullifier(nullifierHash);
// We now verify the provided proof is valid and the user is verified by World ID
worldId.verifyProof(
root,
groupId,
abi.encodePacked(signal).hashToField(),
nullifierHash,
externalNullifier,
proof
);
// We now record the user has done this, so they can't do it again (proof of uniqueness)
nullifierHashes[nullifierHash] = true;
// Finally, execute your logic here, for example issue a token, NFT, etc...
// Make sure to emit some kind of event afterwards!
emit Verified(nullifierHash);
}
}
This contract has all the necessary pieces that any app that wants to integrate World ID proofs of personhood will require. The World ID docs have a detailed explanation of how the World ID system works and how to integrate it into your app. But we will go over the main parts of the contract here:
- The
Contract
contract is the main contract that will be deployed to World Chain Sepolia. It has a constructor that takes theIWorldID
interface, the app ID and the action ID as parameters. TheIWorldID
interface is the World ID router that will verify the proofs, the app ID is the ID of the app can be created by the developer using the World ID Developer Portal and and the action ID is the ID of the action that the user is performing which will be generated automatically by the IDKit SDK and derived fromaction
string defined in the Developer Portal. - The
nullifierHashes
mapping is used to keep track of the nullifier hashes that have been used already. This is used to guarantee that an action is only performed once by a single person in order to achieve sybil resistance. - The
verifyAndExecute
function is the main function that will be called by the user to verify their proof of personhood. It takes the user's wallet address, the root of the Merkle tree, the nullifier hash, and the proof as parameters. - The function first checks if the nullifier hash has been used already and reverts if it has.
- It then verifies the proof using the
worldId.verifyProof
function which is part of theIWorldID
interface. - If the proof is valid, the function records the nullifier hash and executes the logic of the app. In this case, it emits
the
Verified
event.
If you want an example of a production application which uses the World ID protocol, you can check out the
Worldcoin grants contracts.
Specifically, the RecurringGrantDrop.sol
contract which uses the World ID protocol to verify that the user is a unique human before they can claim a grant.
Deploy template app
First, you have to go to a node provider that supports World Chain Sepolia. You can use Alchemy or any of the other providers listed in the World Chain documentation. Once you have a node provider account, you need to get an RPC URL for the World Chain Sepolia network. For a simple deployment, the public RPC URL is sufficient. However, for doing a fork deployment, you will need to run a local fork of World Chain Sepolia using anvil which requires higher requirements on the RPC provider for forking the network.
First, we will fork the World Chain Sepolia network using anvil:
# Substitute the RPC_URL with the RPC URL of the World Chain Sepolia network
anvil -f $RPC_URL
We need to also set three important environment variables that are required for the deployment of the World ID template app:
WORLD_ID_ROUTER
: The address of the World ID router contract that will verify the proofs (can be found in the World ID documentation)NEXT_PUBLIC_APP_ID
: The app ID that was generated in the Developer PortalNEXT_PUBLIC_ACTION
: The action ID as configured in the Developer Portal
Once the fork is running, you can deploy the World ID template app to the World Chain Sepolia network using the Foundry CLI:
# cd into the contracts directory
cd contracts
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
This command will deploy the Contract
contract to the World Chain Sepolia network using the provided RPC URL and private key.
Local Web Setup
Set up your environment variables in the .env
file. You will need to set the following variables:
NEXT_PUBLIC_APP_ID
: The app ID as configured in the Worldcoin Developer Portal.NEXT_PUBLIC_ACTION
: The action ID as configured in the Worldcoin Developer Portal.NEXT_PUBLIC_WALLETCONNECT_ID
: Your WalletConnect ID.NEXT_PUBLIC_CONTRACT_ADDRESS
: The address of the contract deployed in the previous step.
Back in the root directory of the World ID template app, you can start the local web server:
pnpm dev
The Contract ABI will be automatically re-generated and saved to src/abi/ContractAbi.json
on each run of pnpm dev
.
Iterating
After making changes to the contract, you should:
- re-run the
forge create
command from above - replace the
NEXT_PUBLIC_CONTRACT_ADDRESS
environment variable with the new contract address - if your contract ABI has changed, restart the local web server
Testing
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
.
When connecting your wallet to the local development environment, you will be prompted to add the network to your wallet.
Use the Worldcoin Simulator in place of World App to scan the IDKit QR codes and generate the zero-knowledge proofs.
Further resources
If you want to learn more about the World ID protocol, you can check out the World ID documentation. If you want to build an application that uses World ID and targets existing World App users, check out miniapps!