Hi community,
I’ve recently been working on this draft that describes zk-SNARK compatible ERC-721 tokens:
https://github.com/Nerolation/EIP-ERC721-zk-SNARK-Extension
Basically every ERC-721 token gets stored on a Stealth Address that constists of the hash h of a user’s address a, the token ID tid and a secret s of the user, such that stealthAddressBytes = h(a,tid,s). The stealthAddressBytes are inserted into a merkle tree. The root of the merkle tree is maintained on-chain. Tokens are stored at an address that is derived from the user’s leaf in the merkle tree: stealthAddressBytes => bytes32ToAddress().
For transfering a token, the contract requires a proof that a user can
i) generate a stealth address that is included in the merkle tree
ii) generate the merkle tree after updating the respective leaf
For minting a token, the contract requires a proof that a user can
i) generate a stealth address and add it to an empty leaf in the merkle tree
ii) generate the merkle tree after updating the respective leaf
For burning a token, the contract requires a proof that a user can
i) generate a stealth address and delete it from a leaf in the merkle tree
ii) generate the merkle tree after updating the respective leaf
NOTE: The generation of the stealth address requires to have access to a private key. E.g. a user signs a message, the circuit parses the public key (…and address), hashes the address together with the token ID and a secret value and inserts the result into a leaf of the merkle tree. In the end the circuit compares the calculated and user-provided roots for verification.
For general information, have a look at Vitalik’s short section on private POAPs in this article on SoulBound tokens.
I think, this EIP is the exact implementation of what Vitalik described.
This is the current draft of the interface:
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.6;
...
interface ERC0001 /* is ERC721, ERC165 */ {
/// @notice Mints token to a stealth address `stA` if proof is valid. stA is derived from
/// stealthAddressBytes which is the MIMC Sponge hash `h` (220 rounds) of the user address `eoa`,
/// the token id `tid` and a user-generated secret `s`, such that stA <== address() <== h(eoa,tid,s).
/// @dev Requires a proof that verifies the following:
/// - prover can generate the StealthAddress (e.g. user signs msg => computePublicKey() => computeStealthAddress() ).
/// - prover can generate the merkle root from an empty leaf.
/// - prover can generate the merkle root after updating the empty leaf.
/// @param currentRoot A known (historic) root.
/// @param newRoot Updated root.
/// @param stealthAddressBytes Hash of user address, tokenId and secret.
/// @param tokenId The Id of the token.
/// @param proof The zk-SNARK.
function _mint(bytes32 currentRoot, bytes32 newRoot, bytes32 stealthAddressBytes, uint256 tokenId, bytes proof) external;
/// @notice Burns token with specified Id from stealth address `stA` if proof is valid.
/// @dev Requires a proof that verifies the following:
/// - prover can generate the StealthAddress (e.g. user signs msg => computePublicKey() => computeStealthAddress() )
/// - prover can generate the merkle root from an non-empty leaf.
/// - prover can generate the merkle root after nullifieing the non-empty leaf.
/// @param currentRoot A known (historic) root.
/// @param newRoot Updated root.
/// @param stealthAddressBytes Hash of user address, tokenId and secret.
/// @param tokenId The Id of the token.
/// @param proof The zk-SNARK.
function _burn(bytes32 currentRoot, bytes32 newRoot, bytes32 stealthAddressBytes, uint256 tokenId, bytes proof) external;
/// @notice Transfers token with specified Id from current owner to the recipient's
/// stealth address, if proof is valid.
/// @dev Requires a proof that verifies the following:
/// - prover can generate the StealthAddress (e.g. user signs msg => computePublicKey() => computeStealthAddress() ).
/// - prover can generate the merkle root from an non-empty leaf.
/// - prover can generate the merkle root after updating the non-empty leaf.
/// @param currentRoot A known (historic) root.
/// @param newRoot Updated root.
/// @param stealthAddressBytes Hash of user address, tokenId and secret.
/// @param tokenId The Id of the token.
/// @param proof The zk-SNARK.
function _transfer(bytes32 currentRoot, bytes32 newRoot, bytes32 stealthAddressBytes, uint256 tokenId, bytes proof) external;
/// @notice Verifies zk-SNARKs
/// @dev Forwards the different proofs to the right `Verifier` contracts.
/// Different Verifiers are required for each action, because of the merkle-tree logic involved.
/// @param currentRoot A known (historic) root.
/// @param newRoot Updated root.
/// @param stealthAddressBytes Hash of user address, tokenId and secret.
/// @param tokenId The Id of the token.
/// @param proof The zk-SNARK.
/// @return Validity of the provided proof.
function _verifyProof(bytes32 currentRoot, bytes32 newRoot, bytes32 stealthAddressBytes, uint256 tokenId, bytes proof) external returns (bool);
}
This EIP is still in idea stage (no pull-request yet).
Looking for collaborators!