Clean Hands

How to read Clean Hands attestations

Zeronym issues its Clean Hands attestation to users who prove that they are not on any sanctions lists.

See Clean Hands for a description of architecture.

Off-chain with Sign Protocol

Use Sign Protocol's scan API to see whether a user has a valid Clean Hands attestation.

// Set user address
const address = '0x123'

const resp = await fetch(`https://mainnet-rpc.sign.global/api/scan/addresses/${address}/attestations`)
const data = await resp.json()
const cleanHandsAttestations = data.data.rows.filter((att) => (
  att.fullSchemaId == 'onchain_evm_10_0x8' &&
  att.attester == '0xB1f50c6C34C72346b1229e5C80587D0D659556Fd' &&
  att.isReceiver == true && 
  !att.revoked &&
  att.validUntil > (new Date().getTime() / 1000)
))
const hasValidAttestation = cleanHandsAttestations.length > 0

On Optimism with Sign Protocol

Tutorial coming soon...

On-chain (not Optimism)

Use our off-chain attester, and verify its signature on-chain. Our attester returns a signature of the circuit ID, action ID, and user address, if the address has a clean hands attestation.

Our attester address is 0xa74772264f896843c6346ceA9B13e0128A1d3b5D.

Query for signature

// Set user address
const userAddress = '0x123'

const actionId = 123456789
const resp = await fetch(`https://api.holonym.io/attestation/sbts/clean-hands?action-id=${actionId}&address=${userAddress}`)
const { isUnique, signature, circuitId } = await resp.json();

Verify signature in Solidity

Warning: this Solidity code is untested.

pragma solidity ^0.8.4;

import "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";

contract SignatureVerification {
    using ECDSA for bytes32;

    address public zeronymAttester = 0xa74772264f896843c6346ceA9B13e0128A1d3b5D;

    function verifySignature(
        uint256 circuitId,
        uint256 actionId,
        address userAddress,
        bytes memory signature
    ) public pure returns (bool) {
        bytes32 digest = keccak256(abi.encodePacked(
            circuitId,
            actionId,
            userAddress
        ));

        bytes32 personalSignPreimage = keccak256(abi.encodePacked(
            "\x19Ethereum Signed Message:\n32",
            digest
        ));

        (
            address recovered,
            ECDSA.RecoverError err,
            bytes32 _sig
        ) = ECDSA.tryRecover(personalSignPreimage, publicSignalsSig);

        return recovered == zeronymAttester;
    }
}

Verify signature in JavaScript

// Verify using ethers v5
const digest = ethers.utils.solidityKeccak256(
  ["uint256", "uint256", "address"],
  [circuitId, actionId, userAddress]
);
const personalSignPreimage = ethers.utils.solidityKeccak256(
  ["string", "bytes32"],
  ["\x19Ethereum Signed Message:\n32", digest]
);
const recovered = ethers.utils.recoverAddress(personalSignPreimage, signature)
console.log(recovered === '0xa74772264f896843c6346ceA9B13e0128A1d3b5D')

Last updated