Worldcoin

Technical Reference

Smart Contracts

All of our smart contracts are available on GitHub:

If you're interested in using World ID and verifying proofs on-chain, see our On-Chain Verification guide.

Supported Chains

ChainTestnetRoleIdentity Availability
Ethereum logo

Ethereum

Goerli

Canonical

~3 Minutes

Optimism logo

Optimism

Optimism Goerli

Bridged

~5 Minutes

Polygon logo

Polygon

Mumbai

Bridged

~30 Minutes

Architecture

This section offers a high-level overview of the various smart contracts that make up World ID. This structure (including state bridging) is replicated on testnets -- currently Goerli, Optimism Goerli, and Polygon Mumbai.

World ID Router

This is the contract you should interact with. It will route your call to the correct Identity Manager contract (Ethereum) or State Bridge contract (L2 Chains) based on the groupId argument. This contract is proxied, so you should not need to update your code if we upgrade the underlying contracts.

Identity Managers

Identity Managers are only deployed on Ethereum. One contract is deployed for each credential type accepted in World ID, currently two are deployed: Orb and Phone.

The Identity Manager contracts are responsible for managing the Semaphore instance. Worldcoin's signup sequencers call the Identity Manager contracts to add identities to the merkle tree, and anyone can call the verifyProof function to verify a World ID proof (although it's suggested to use the World ID Router).

State Bridges

On Ethereum, one State Bridge contract is deployed for each Identity Manager. It publishes the root of the merkle tree to other chains, allowing proofs to be verified on multiple chains.

On other supported chains (currently Optimism and Polygon), there is also one State Bridge contract for each credential type. These contracts receive the root of the merkle tree from the Ethereum State Bridge, and expose the verifyProof function to verify proofs on that chain (using the World ID Router is recommended).

Address Book

Here you can find the address and associated ENS name (if available) for all of the World ID contracts. For verifying proofs, the only contract you need is the WorldIdRouter contract -- it will properly route the call to the correct contract based on the groupId argument.

verifyProof

The verifyProof function is meant to be called on the WorldIdRouter contract.

The verifyProof method takes the following arguments:

  • root - The World ID root to verify against. This is obtained from the IDKit widget, and should just be passed as-is.
  • groupId - This must be 1 for Orb-verified users, and 0 for Phone-verified users. You may pass this dynamically based on a user's verification status, or you may set it during contract deployment it if you only want to allow one type of verification.
  • signal - The signal to verify.
  • nullifierHash - Anonymous user ID. This is obtained from the IDKit widget, and should just be passed as-is.
  • action - The action to verify.
  • proof - The proof to verify. This is obtained from the IDKit widget, and should be unpacked into a uint256[8] before being passed to the method.

root

The root of the merkle tree to verify against. This is obtained from the IDKit widget as a hex string merkle_root, and should be passed as-is.

groupId

The groupId, indicating to the World ID Router whether to verify against the merkle tree of Orb- or Phone-verified users.

Orb-verified users: 1
Phone-verified users: 0

We recommend setting this to 1 in your contract's constructor, to only ever allow Orb-verified users to perform the specified action. Additionally, this saves on gas costs.

However, if you wish to allow Orb- and Phone-verified users, IDKit returns a credential_type field, which is either phone or orb. You can use this to determine the groupId to use in your call to verifyProof.

groupId

uint256 internal immutable groupId = 1;

{/* ... */}

worldId.verifyProof(
  root,
  groupId,
  abi.encodePacked(signal).hashToField(),
  nullifierHash,
  externalNullifier,
  proof
);

{/* ... */}

signalHash

The keccak256 hash of the signal to verify. To get signalHash, you should pass the solidityEncoded signal to your smart contract, and then compute the signalHash within the contract. Ensure that you solidityEncode the signal before passing it to IDKit.

We provide a helper function hashToField to properly calculate the keccak256 hash within your smart contract.

signalHash

function yourFunction(
  uint256 root,
  address signal, // here we use an address as the signal
  uint256 nullifierHash,
  uint256[8] calldata proof
) public {

  {/* ... */}

  worldId.verifyProof(
    root,
    groupId,
    // using hashToField helper function
    abi.encodePacked(signal).hashToField(), 
    nullifierHash,
    externalNullifier,
    proof
  );

  {/* ... */}

nullifierHash

The root of the merkle tree to verify against. This is obtained from the IDKit widget as a hex string merkle_root, and should be passed as-is.

externalNullifierHash

The root of the merkle tree to verify against. This is obtained from the IDKit widget as a hex string merkle_root, and should be passed as-is.

proof

The proof argument is returned from IDKit as a string, but depending how you're calling your smart contract (when using wagmi or ethers.js, for example), you might be required to unpack it into a uint256[8] before passing it to the verifyProof method. To unpack it, use the following code:

unpackedProof

import { decodeAbiParameters } from 'viem'

const unpackedProof = decodeAbiParameters([{ type: 'uint256[8]' }], proof)[0]