Blog:
Proof-of-HUMANity on-chain: protect your smart contracts from bots
Work by Alex Bakoushin, edited by HUMAN Protocol
This article will demonstrate how developers can use Proof of HUMANity to protect their DApps from bots.
The article will cover how smart contracts are vulnerable to bot activity, and offer a solution to developers: Proof-of-HUMANity.
Let’s consider the simplest example dApp — a counter stored on-chain.
Demo: basic-counter.bakoush.in
Note. All demos in this article are deployed on Polygon Mumbai Testnet. Consider getting some test MATIC via their faucet.
Humans can increase the counter by pressing the button in the UI. Bots, instead, could leverage a direct call to the smart contract. This is an example that holds true across many smart contracts.
const increment = async () => {
console.log('🤖 Incrementing counter...'); // Just call the method...
const tx = await counter.increment();
await tx.wait(); const value = await counterContract.counter();
console.log(`✅ Done. New value is ${value}`);
};
If you want to play with the bot yourself, here is the address of the basic counter contract (Polygon Mumbai Testnet): 0x336c94E1F0F4D0103b012E78E6700959c89Ba8AD
Depending on your app design, you may want to prevent similar bots from interacting with your app.
This is where the problem of bot protection arises.
Given that everyone can interact with smart contracts’ public methods, it is not enough to integrate traditional CAPTCHA solutions into the UI or backend servers. We have to additionally check humanity on-chain, within the smart contract itself — which has no ability to interact with the outside world, and thus can not call any off-chain APIs.
We propose a simple, yet powerful mechanism to pass the Proof of HUMANity verification to on-chain infrastructure.
Proof-of-HUMANity is signed evidence of the caller being a human. Signed off-chain by a trusted party, PoH could be then verified on-chain.
PoH consists of a proof base 36 or 97 bytes long depending on the type of proof, and a 65-bytes validator signature over the proof base.
Proof-of-Humanity
proof base | validator
| signature
---------------+----------
36 or 97 bytes | 65 bytes
On-chain one can verify the validator signature and thus trust that the humanity of the caller is authentically confirmed.
Proof-of-HUMANity can be of two types: basic and sovereign.
The basic proof has a base of 36 bytes and thus is 101 bytes long. It consists of a random challenge and a timestamp as a proof base, along with a validator signature over the proof base.
Basic PoH
random | timestamp | validator
challenge | | signature
----------+-----------+----------
32 bytes | 4 bytes | 65 bytes
Sovereign PoH
The sovereign proof has a base of 97 bytes and thus is 166 bytes long. In addition to basic PoH elements, its base includes the sender's signature.
This way, one can check on-chain that the proof is generated by the transaction sender itself, eliminating the possibility of generating proofs by those not in control of the sender’s address.
This provides more robustness, but requires that users sign the challenge with their wallet.
Sovereign PoH
random | sender | timestamp | validator
challenge | signature | | signature
----------+-----------+-----------+----------
32 bytes | 65 bytes | 4 bytes | 65 bytes
When a user wants to send a transaction to your smart contract, your app UI could get a Proof-of-HUMANity verification using the hCaptcha system. This proof then could be used in the smart contract call.
The smart contract, in turn, verifies that the proof comes from the trusted source and has not been seen before. Otherwise, it reverts.
Integrating PoH into your app is fairly easy thanks to existing libraries. It takes 3 steps:
The key element of the Proof-Of-HUMANity is the validator signature. You must trust that this validator is actually validating users’ humanity. This can be achieved in at least two ways:
The latter option is not yet available (but we hope it will), so let’s focus on the former one.
You can quickly deploy an API producing the Proof-of-HUMANity using a Docker image. It is an example of PoH validator API for the hCaptcha service.
docker run -it \
-p 8080:8080 \
--env PORT=8080 \
--env VALIDATOR_KEY=<Validator private key> \
--env HCAPTCHA_SECRET=<hCaptcha secret> \
bakoushin/poh-validator-hcaptcha
You have to provide hCaptcha secret and the validator private key. This key will be used to sign proofs.
You can also create your own validator, as long as it produces the valid proofs adhering to the PoH format we discussed earlier.
To interact with the deployed API, you can use a set of React components designed to be quickly integrated into any app:
1) Wrap your app into the ProofOfHumanityProvider:
import { ProofOfHumanityProvider } from ‘poh-react’;<ProofOfHumanityProvider>
<App />
</ProofOfHumanityProvider>;
2) Instantiate the PoH hCaptcha validator plugin:
import hCaptchaValidator from ‘poh-validator-hcaptcha’;const validator = (
<HCaptchaValidator
sitekey="10000000-ffff-ffff-ffff-000000000001"
url="http://localhost:3000/api/v1/proof"
/>
);
3) Initialize the getProofOfHumanity method from the PoH hook using the instantiated validator:
import { useProofOfHumanity } from ‘poh-react’;const { getProofOfHumanity } = useProofOfHumanity(validator);
4) Obtain the Proof-Of-HUMANity prior to sending any sensitive transaction, and provide it as a parameter to the sensitive external method you are calling:
const handleClick = () => {
try {
const {
error,
errorMessage,
proof
} = await getProofOfHumanity();
if (!error) {
const tx = await mySmartContract.someImportantMethod(proof);
}
} catch(error) {
console.error(error);
}
}
<button onClick={handleClick}>Send transaction</button>
That’s it for the UI!
You can use poh-contracts library to easily integrate Proof-Of-HUMANity into your smart contracts.
Let’s look at an example. Here is our basic counter contract:
contract Counter {
uint256 public counter;
event Increment(uint256 currentCounter);
function increment() public {
counter++;
if (counter > 99) {
counter = 1;
}
emit Increment(counter);
}
}
Here is how we can integrate it with Proof-Of-HUMANity:
import "poh-contracts/contracts/HumanOnly.sol";contract Counter is HumanOnly {
uint256 public counter;
event Increment(uint256 currentCounter); constructor() {
setHumanityValidator(
0x9064071eaB7c22E00e2d63233a9507d7107cFCD1
);
} function increment(bytes calldata proof) public basicPoH(proof) {
counter++; if (counter > 99) {
counter = 1;
}
emit Increment(counter);
}
}
That’s it for the smart contract!
Now, the method call will revert unless the proof has a valid signature and has not been seen before.
Demo: poh-counter.bakoush.in
If you are eager to try to bot this one, here is the address (Polygon Mumbai Testnet): 0x454C82492DF9E5582186c983D26Dda6Bf9861A50
dApp Example
Counter dApp
Source code
Validator API (Docker)
Proof-of-Humanity hCaptcha Validator API
UI Components
Proof-of-Humanity-React
Proof-of-Humanity hCaptcha Validator React
Solidity Library
Proof-of-Humanity Solidity Contracts