Polygon ID Part-3 : On chain verification by Verifier

Atharva Paliwal
Coinmonks
Published in
8 min readDec 10, 2022

--

Source : Polygon ID Docs

This is the third article of the on going series on Polygon ID where we will cover about the On chain verification by Verifier.

However if you are directly starting with this article, I would advice you to have a read to Part-1 & Part-2 of the series that covers the core concepts and issuance of claims respectively.

I will continue this article from where we left in Part-2, where we had issued the claim to the holder. In this article we will set up the request by Verifier who will verify whether the Claim holder satisfies the proof request or not.

To start with the tutorial, I would request you to clone this repository. It contains the full implementation of the code discussed in the article.

Designing the ERC20 zk Contract

The ERC20Verifier is an ERC20 standard contract on steroids. The extra functionality is given by the zero-knowledge proof verification. All the functions dedicated to the zk verification are contained inside the ZKPVerifier Contract and inherited within the ERC20Verifier.

For example, users will submit their proof to claim the tokens by calling submitZKPResponse function.

The ERC20Verifier contract must define at least a single TRANSFER_REQUEST_ID which resembles the Identifier of the request that the contract is posing to the user.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./lib/GenesisUtils.sol";
import "./interfaces/ICircuitValidator.sol";
import "./verifiers/ZKPVerifier.sol";

contract ERC20Verifier is ERC20, ZKPVerifier {

uint64 public constant TRANSFER_REQUEST_ID = 1;

mapping(uint256 => address) public idToAddress;
mapping(address => uint256) public addressToId;

uint256 public TOKEN_AMOUNT_PER_ID = 5 * 10**uint(decimals());

constructor(string memory name_, string memory symbol_)
ERC20(name_, symbol_)
{}

function _beforeProofSubmit(
uint64, /* requestId */
uint256[] memory inputs,
ICircuitValidator validator
) internal view override {
// check that challenge input of the proof is equal to the msg.sender
address addr = GenesisUtils.int256ToAddress(
inputs[validator.getChallengeInputIndex()]
);
require(
_msgSender() == addr,
"address in proof is not a sender address"
);
}

function _afterProofSubmit(
uint64 requestId,
uint256[] memory inputs,
ICircuitValidator validator
) internal override {
require(
requestId == TRANSFER_REQUEST_ID && addressToId[_msgSender()] == 0,
"proof can not be submitted more than once"
);

uint256 id = inputs[validator.getChallengeInputIndex()];
// execute the logic
if (idToAddress[id] == address(0)) {
super._mint(_msgSender(), TOKEN_AMOUNT_PER_ID);
addressToId[_msgSender()] = id;
idToAddress[id] = _msgSender();
}
}

function _beforeTokenTransfer(
address, /* from */
address to,
uint256 /* amount */
) internal view override {
require(
proofs[to][TRANSFER_REQUEST_ID] == true,
"only identities who provided proof are allowed to receive tokens"
);
}
}

In the contract you would notice two hooks —

  1. _beforeProofSubmit
  2. _afterProofSubmit

Let us see why this two hooks are important.

_beforeProofSubmit checks that the sender of the proof matches the address contained in the proof challenge.

_afterProofSubmit contains the logic that we need to write in our use case. It also contains the condition that tells that for particular TRANSFER_REQUEST_ID proof is executed only once.

Moving ahead, we notice _beforeTokenTransfer that is overrided by ERC20 which prevents any type of token transfer (even after the airdrop) unless users passed the proof verification.

This completes our ERC20Verifier contract. Let’s now move towards the deployment of the contract.

Deploying the ERC20Verifier contract and Setting up the ZKP request

In the cloned repository, you will see the two hardhat scripts, where one script contains the deployment of contract and another script contains the setting up of ZKP request.

Let’s setup the set-request.js file which will setup the proof request for the verifier.

If you will notice in the set-request.js file, function setZKPRequest is called from the ERC20Verifier contract which takes in 3 parameters —

  1. requestID
  2. validator
  3. query

requestID defines the ID that is associated with the request.

validator here refers to the validator smart contract deployed on Mumbai testnet. This is the contract that actually executes the verification on the zk proof submitted by the user.

query here refers to the set of rules that verifier specifies in his proof request for the identity holder.

query is an object that contains some parameters —

  1. schema
  2. slotIndex
  3. operator
  4. value
  5. circuitID

schema is the hash of the schema that you can retrieve from the issuer dashboard on Polygon ID Platform. In order to use it inside the query it should be converted from hex to bigint.

NOTE: If you would have been a early user of Polygon ID docs, the schema hash conversion was not mentioned in the docs which caused a lot of errors in the initial stages. However not it got resolved :)

slotIndex is the index of the attribute you are querying. Note that while creating a claim we mention the can mention upto 2 attributes. In our case we mentioned only one that was Age (refer to my Part-2 of the Article series, if you are confused about Attributes).

slotIndex value can be either 2 or 3 where 2 refers to the Attribute-1 and 3 refers to Attribute-2. In our case since we stored the Age information in Attribute-1 so we will take the slotIndex value as 2.

operators can range from 1 to 5 where each operator means —

operator 1 : equals

operator 2 : less than

operator 3 : greater than

operator 4 : in

operator 5 : not in

So you can chose which operator you want according to your use case, In our scenario we will chose operator 3 as we want all the Identity holders whose age is greater than 18.

value represents the threshold value you are querying. In our case the threshold value is 18 as we want all the users to be >18 years of age.

circuitID is the ID of the circuit you are using for verification. It can be either credentialAtomicQueryMTP or credentialAtomicQuerySig. We have used credentialAtomicQuerySig for the demo purpose.

Now you are all set with your set-request.js file. But before proceeding make sure you have made the following changes in the github repo file —

  1. Changed the schema hash (you can get it from Polygon ID platform, make sure you copy the hash of the schema you are wanting to verify)
  2. Change the query parameters : operator & value
  3. Add the ERC20Verifier contract deployed address (just run the deploy.js file)

Now you can successfully set your ZKP request. If you will open your ERC20Verifier contract address on Polygonscan (in case you have deployed your contract on Polygon) you might notice that our ZKP request is set successfully.

SetZKPRequest

Now let’s add this proof request inside a QR code and let our user pass through the proof request.

Setting the Proof Request Inside the QR code

Let’s head towards the frontend folder where in App.js file we already have the QR code ready but we need to make some changes so that QR code now contains our proof request.

  1. Change the contract address with your deployed ERC20Verifier contract address.
  2. Inside qrProofRequestJson under req add the attribute name you gave while making your claim. In our case it was Age. Then you can gave the operator whether it is less than request or a greater than request along with the threshold value.
  3. Under schema section you need to specify the schema URL and the type that is the name you gave to your schema. In our case it was AgeCredential.
id: "c811849d-6bfb-4d85-936e-3d9759c7f105",
typ: "application/iden3comm-plain-json",
type: "https://iden3-communication.io/proofs/1.0/contract-invoke-request",
body: {
transaction_data: {
contract_address: deployedContractAddress,
method_id: "b68967e2",
chain_id: 80001,
network: "polygon-mumbai"
},
reason: "ERC20 Transfer using On chain Verification",
scope: [
{
id: 1,
circuit_id: "credentialAtomicQuerySig",
rules: {
query: {
allowed_issuers: ["*"],
req: {
Age: {
$gt: 18
}
},
schema: {
url:
"https://s3.eu-west-1.amazonaws.com/polygonid-schemas/33f9238b-dad0-440e-aa20-4561606c289b.json-ld",
type: "AgeCredential"
}
}
}
}
]
}

Now we are all setup and let’s verify our user with the verifier’s proof request whether he is >18 years of age or not.

Open your Polygon ID wallet app and scan the QR code that would be generated in your react app.

You will see the process like this —

After this check on Polygonscan we will see —

Our request is successfully submitted and our user got the ERC20Verfier tokens as he was >18 years of age.

So this calls to an end to the Polygon ID introduction series which covered about the core concepts , issuance of claims, verification of claims in detail.

My advice to those who have followed the tutorial will be to try out some different use cases and use Polygon ID in your dapps.

Do tag me on twitter if you find my article useful or if you are taking this concept and using somewhere in your dapps. Do give me a clap or leave a comment on medium if you find this article useful. You can also find me on Linkedin and do share it with you friends who want to learn about Polygon ID.

--

--

Atharva Paliwal
Coinmonks

Blockchain Developer (R&D) at Persistent Systems | Writes about new and less known things that you may want to add to your skills portfolio ✍️💫