Skip to content

jimjimvalkema/scrollZkWormholes

Repository files navigation

example of a erc20 with EIP7503

An erc20 token with EIP7503 (zkwormholes) style private transfers.
Using storage proofs to track the balances of the burn addresses instead of commitments in a merkle tree.

Try it out here: https://scrollzkwormholes.jimjim.dev/
Or on ipfs: https://bafybeichjrwquiabspceeyvz5mkysszjno5cgj6a4vrfp2sthsfiszehjy.ipfs.dweb.link/

ui

deploymed on scroll sepolia

https://sepolia.scrollscan.com/address/0x12b65F787D7A4672218cA4375f79133564328B28

WARNING

This version is not mainnet ready since it uses a workaround that allows ANYONE to mint free tokens.
This because the BLOCKHASH opcode isn't ready yet on scroll.
See: https://github.com/jimjimvalkema/scrollZkWormholes/blob/main/contracts/Token.sol#L27

How it works

plausible deniability

EIP7503 doesn't reveal if an user sends a private transfer. Instead it looks like a "normal" transfer.
In contrast to protocols like zcash, tornadocash, etc, where it's publicly "announced" when a user with public money protects their privacy.
In EIP7503, such actions look like normal transfers to fresh new wallets. Under the hood these wallets do not have a private key, so these funds are burned. The protocol then allows the user to "re-mint" these burned coins to an unlinked address, only if they can provide the zk-proof that no private key exist for the burn address.

gas cost

The gas cost of burning tokens is the same as a vanilla erc20 transfer. The equivalent is a deposit in ex tornado cash, which can cost up to 16x more.
This is because we reused the state tree as our merkle tree of commitments with storage proofs. Unlike in tornadocash and railgun where the contract builds it's own merkle tree.

storage proofs

The storage proofs are used as a private input to prove that an address with a balance (a public input) exist onchain.
The storage proofs are done on scroll which a zk-rollup. Which means its state trie uses zk friendly hash functions. Which makes it a lott faster to prove then Ethereums state trie!

burn address

The burn address is a private input, generated by hashing a secret with the poseidon hash function and then striping of the last 12 bytes. This means that there is no private-public key pair of that hash since it calculated "wrong". But there is a secret which can be proven the user knows in a zk-circuit without revealing it!

function hashBurnAddress(secret) {
    const hash = ethers.toBeArray(poseidon1([secret])) 
    const burnAddress = hash.slice(0,20)
    return ethers.zeroPadValue(ethers.hexlify(burnAddress),20)
}

at line 152-157 in scripts/getProofInputs.js
https://github.com/jimjimvalkema/scrollZkWormholes/blob/main/scripts/getProofInputs.js#L152

nullifier

The nullifier a public input, which is a hash of the secret + hash of secret. So: poseidon(secret, poseidon(secret)).
This is used to as a identifier to prevent double spending the same secret again. Without revealing the burnaddress that is nullified.

function hashNullifier(secret) {
    const hashedSecret = poseidon1([secret]) 
    const nullifier = poseidon2([secret,hashedSecret])
    return ethers.zeroPadValue(ethers.toBeHex(nullifier),32)
}

at line 146-150 in scripts/getProofInputs.js
https://github.com/jimjimvalkema/scrollZkWormholes/blob/main/scripts/getProofInputs.js#L146

what's next

The original EIP doesn't specify this but there are a number of features i like to add.

re-usable burn address + partial re-mints: Might be possible by tracking totalAmountReminted inside the nullifier pre-image. Then also adding storage proof of the prev nullifier.
full shielding txs: Like in zcash and railgun, etc.
improve proving time: with proof recursion and a historicStorageRoots in a public mapping in the contract.

run ui locally

install

yarn install;
yarn install-submodules && yarn install-vite;

run locally

yarn dev

build static site locally

yarn build

deploy

set environment variables

yarn hardhat vars set PRIVATE_KEY; #<=deployment key
yarn hardhat vars set SEPOLIA_SCROLL_ETHERSCAN_KEY;
yarn compile-contracts;

deploy

yarn hardhat run scripts/deploy.cjs --network scrollSepolia;
yarn hardhat ignition deploy ignition/modules/Token.cjs --network scrollSepolia --verify #couldnt verify within deploy.cjs so this is a hacky work around

test remint sepoilia

set reminter privatekey (can be same as deployer)

yarn hardhat vars set RECIPIENT_PRIVATE_KEY;

do remint

yarn hardhat run scripts/proofAndRemint.js 

test circuit

Install noir

curl -L https://raw.githubusercontent.com/noir-lang/noirup/refs/heads/main/install | bash
noirup -v 0.31.0

Run test

cd circuits/smolProver;
nargo test;

Compile circuit (verifier contracts are created in scripts/deploy.cjs)

yarn compile-circuits 

notes

smolprover and fullProver

The contract uses 2 verifiers: smolprover and fullProver. This is because noirjs has a limit on ram (4gb), so smollProver uses smaller parameters that assume the chain doesnt becomes larger.
FullProver is there just incase the chain grows outside of the parameters of smolprover.
More specifically full prover is able to go down the full 248 tree depth and can handle a blockheader up to 850 bytes.
While smolprover is set to tree depth of 26 and 650 bytes for the blockheader

global MAX_HASH_PATH_SIZE = 26;
global MAX_RLP_SIZE = 650;

at line 13-14 in circuits/smolprover/src/main.nr https://github.com/jimjimvalkema/scrollZkWormholes/blob/main/circuits/smolProver/src/main.nr#L13

no BLOCKHASH

The BLOCKHASH opcode on scroll isnt ready yet so the contract relies on a trusted oracle (the deployer) to tell the contract what blockhash is valid.

different blockheaders

sepolia scroll block header is different then scroll mainnet. It includes baseFeePerGas, while mainnet scroll doesnt yet.
modify ../scripts/getScrollProof.js at getBlockHeaderRlp() for mainnet

    const headerData = [
        block.parentHash,       
        block.sha3Uncles,       
        block.miner,             
        block.stateRoot,         
        block.transactionsRoot, 
        block.receiptsRoot,      
        block.logsBloom,       
        block.difficulty,       
        block.number,           
        block.gasLimit,                 
        block.gasUsed,            
        block.timestamp,                                
        block.extraData,       
        block.mixHash,                 
        block.nonce,            

        // is in scroll-sepolia but not mainnet yet
        block.baseFeePerGas,

        // in neither chains
        // block.withdrawalsRoot,
        // block.blobGasUsed,
        // block.excessBlobGas,
        // block.parentBeaconBlockRoot

    ]

at line 259-283 ../scripts/getScrollProof.js https://github.com/jimjimvalkema/scrollZkStorageProofs/blob/main/scripts/getScrollProof.js#L259

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published