Worldcoin

Anonymous Actions

Verifying Proofs On-Chain

World ID proofs can be fully verified on-chain. After all, the source of truth for the decentralized protocol is on-chain. To verify a World ID proof, your smart contract will embed a call to the verifyProof method of the World ID contract, and then execute the rest of its logic as usual.

We provide a smart contract starter kit (Foundry, Hardhat) and a frontend & on-chain monorepo template to help you get started with World ID. We strongly recommend using one of these repositories to get started with World ID on-chain.

The following examples demonstrate the most common use case: verifying proofs from only Orb-verified users, for a single action, with a user's wallet address as the signal, while also enabling sybil-resistance. This setup is recommended for most users, as it is the most gas-efficient. For more information on use cases that require more complex setups (such as multiple actions, other types of signals, or multiple credential types), see the Advanced On-Chain Verification page.

At the core of the World ID Protocol is the use of Semaphore. Semaphore is a zk-SNARK based privacy protocol that allows for the creation of anonymous credential systems developed by the Ethereum Foundation. Read more about The Protocol and Semaphore.

IDKit Configuration

When verifying proofs on-chain, there are a few changes you have to make to your IDKit configuration. Primarily, we must use the solidityEncode function to encode the action parameter. This is because the action parameter is passed to the verifyProof method as a uint256 value, so we must encode it as such.

Additionally, we highly recommend to only allow Orb-verified users to verify on-chain. This is because Orb verification is the only verification method that is strongly sybil-resistant.

import { IDKitWidget, solidityEncode } from '@worldcoin/idkit' // add import for solidityEncode

const { address } = useAddress() // get the user's wallet address

<IDKitWidget
    app_id="app_GBkZ1KlVUdFTjeMXKlVUdFT" // must be an app set to on-chain
    action={solidityEncode(['uint256'], ["claim_nft"])} // solidityEncode the action
    signal={address} // only for on-chain use cases, this is used to prevent tampering with a message
    onSuccess={onSuccess}
    // no use for handleVerify, so it is removed
    credential_types={['orb']} // we recommend only allowing orb verification on-chain
    enableTelemetry
>
    {({ open }) => <button onClick={open}>Verify with World ID</button>}
</IDKitWidget>

Contract Addresses

The World ID Router contract is what you should use to verify proofs. It is deployed on multiple chains, and you can find the addresses for each chain below.

Ethereum Mainnet: id.worldcoin.eth
Ethereum Goerli Testnet: goerli.id.worldcoin.eth
Optimism Mainnet: optimism.id.worldcoin.eth
Optimism Goerli Testnet: op-goerli.id.worldcoin.eth
Polygon Mainnet: polygon.id.worldcoin.eth
Polygon Mumbai Testnet: mumbai.id.worldcoin.eth

For more information on the smart contracts used by World ID, see the Contracts Reference page.

hashToField helper function

We recommend including this helper function in your smart contract. It properly computes the keccak256 hash used by Semaphore, and is frequently used in the example code below.

function hashToField(bytes memory value) internal pure returns (uint256) {
    return uint256(keccak256(abi.encodePacked(value))) >> 8;
}

Constructor

The externalNullifier is the unique identifier of the action performed in Semaphore. It is a combination of the app ID and the action. We set it in the constructor to save gas, as it will not change when all users perform the same action.

We additionally set the groupId to 1, which limits this example to Orb-verified users only.

/// @notice Thrown when attempting to reuse a nullifier
error InvalidNullifier();

/// @dev The address of the World ID Router contract that will be used for verifying proofs
IWorldID internal immutable worldId;

/// @dev The externalNullifier (unique identifier of the action performed), combination of appId and action
uint256 internal immutable externalNullifier;

/// @dev The World ID group ID (1 for Orb-verified, 0 for Phone-verified)
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 _worldId The WorldID instance 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 _action
) {
    worldId = _worldId;
    externalNullifier = abi
        .encodePacked(abi.encodePacked(_appId).hashToField(), _action)
        .hashToField();
}

verifyProof

The verifyProof method reverts if the proof is invalid, meaning you can just call it as part of your smart contract's logic and execute the rest of your logic after as usual.

Note that calling the verifyProof function by itself does not provide sybil-resistance, or prevent proof reuse -- it just verifies that the proof is valid.

However, this example does implement sybil-resistance by checking that the nullifierHash has not been verified before.

The verifyProof method takes the arguments below.

  • root - The World ID root to verify against. This is obtained from the IDKit widget, and should be passed as-is.
  • groupId - This must be 1 for Orb-verified users, and 0 for Phone-verified users.
  • signalHash - The keccak256 hash of the signal to verify.
  • nullifierHash - Anonymous user ID for this action. This is obtained from the IDKit widget, and should just be passed as-is.
  • externalNullifierHash - The keccak256 hash of the externalNullifier set in the constructor.
  • proof - The proof to verify. This is obtained from the IDKit widget.
/// @param signal An arbitrary input from the user that cannot be tampered with. In this case, it is the user's wallet address.
/// @param root The root (returned by the IDKit widget).
/// @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,
    uint256 root,
    uint256 nullifierHash,
    uint256[8] calldata proof
) public {
    // First, we make sure this person hasn't done this before
    if (nullifierHashes[nullifierHash]) revert InvalidNullifier();

    // We now verify the provided proof is valid and the user is verified by World ID
    worldId.verifyProof(
        root,
        groupId, // set to "1"
        abi.encodePacked(signal).hashToField(),
        nullifierHash,
        abi.encodePacked(externalNullifier).hashToField(),
        proof
    );

    // We now record the user has done this, so they can't do it again (sybil-resistance)
    nullifierHashes[nullifierHash] = true;

    // Finally, execute your logic here, knowing the user is verified
}

All arguments are of type uint256, with the exception of proof, which is of type uint256[8]. Depending on how you're calling your smart contract, 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:

import { decodeAbiParameters } from 'viem'

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