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
Chain | Testnet | Role | Identity Availability |
---|---|---|---|
Ethereum | Goerli | Canonical | ~3 Minutes |
Optimism | Optimism Goerli | Bridged | ~5 Minutes |
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 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
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 be1
for Orb-verified users, and0
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 auint256[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 usednullifierHash
es.
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...
}