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

GoerliCanonical~3 Minutes
Optimism logo

Optimism

Optimism GoerliBridged~5 Minutes
Polygon logo

Polygon

MumbaiBridged~30 Minutes
Find our smart contract address book here.

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 the underlying contracts need to be upgraded.

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).

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

Recommendation: Set 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.

A helper function hashToField is provided to properly calculate the keccak256 hash within your smart contract.

signalHash

function yourFunction(
  uint256 root,
  address signal, // in this case the address is used as 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]

Sybil resistance

While the World ID protocol makes it very easy to make your contracts sybil resistant, this takes a little more than just calling the verifyProof function. To make your contract sybil-resistant, you'll need to do the following:

  • Store the nullifierHash of each user that has successfully verified a proof.
  • When a user attempts to verify a proof, check that the nullifierHash is not already in the list of used nullifierHashes.

Here's an example function doing the above. You can also use the World ID starter kits to get started with sybil resistance.

/// @param root The root (returned by the IDKit widget).
/// @param groupId The group ID (returned by the IDKit widget).
/// @param signal An arbitrary input from the user, usually the user's wallet address
/// @param nullifierHash The nullifier hash for this proof, preventing double signaling (returned by the IDKit widget).
/// @param proof The zero-knowledge proof that demonstrates the claimer is registered with World ID (returned by the IDKit widget).
function verifyAndExecute(
    address signal,
    uint8 groupId,
    uint256 root,
    uint256 nullifierHash,
    uint256[8] calldata proof
) public {
    // First make sure this person hasn't done this before
    if (nullifierHashes[nullifierHash]) revert InvalidNullifier();

    // Now verify the provided proof is valid and the user is verified by World ID
    worldId.verifyProof(
        root,
        1, // Or `0` if you want to check for phone verification only
        abi.encodePacked(signal).hashToField(),
        nullifierHash,
        abi.encodePacked(appId).hashToField(),
        proof
    );

    // 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...
}