Skip to main content

EVM Integration

FHE-powered private token vaults on EVM-compatible chains.

V0.1 Alpha

This is an early release of EVM support. It is for testing purposes only — do not use with real value. APIs may change.


Overview

Private Token Slots for EVM lets you build confidential token vaults on Ethereum, Arbitrum, Base, Optimism, and other EVM chains where:

  • Balances are encrypted (no one sees how much anyone holds)
  • Transfers happen privately (sender, receiver, amount all hidden)
  • Only the vault owner can decrypt balances
┌─────────────────────────────────────────────────────────┐
│ Your Token Vault │
├─────────┬─────────┬─────────┬─────────┬────────────────┤
│ Slot 0 │ Slot 1 │ Slot 2 │ Slot 3 │ ... Slot 255 │
│ [ENC] │ [ENC] │ [ENC] │ [ENC] │ [ENC] │
└─────────┴─────────┴─────────┴─────────┴────────────────┘
↓ Transfer 50 from Slot 0 → Slot 1
┌─────────┬─────────┬─────────┬─────────┬────────────────┐
│ [ENC] │ [ENC] │ [ENC] │ [ENC] │ [ENC] │
└─────────┴─────────┴─────────┴─────────┴────────────────┘
All balances remain encrypted throughout!

How It Works

Architecture

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│ Your dApp │ │ EVM │ │ ZKSDK │
│ │ │ (Sepolia) │ │ Coprocessor │
│ │ │ │ │ │
│ 1. Generate │ │ │ │ │
│ FHE keys │────────────────────────→│ 2. Register │
│ │ │ │ │ keys │
│ 3. Deploy │ │ │ │ │
│ Vault │────→│ TokenVault │ │ │
│ │ │ │ │ │
│ 4. Encrypt │ │ │ │ │
│ operation │ │ │ │ │
│ (transfer │ │ 5. Submit │ │ │
│ or withdraw) │────→│ operation │────→│ 6. Compute │
│ │ │ │ │ on FHE │
│ │ │ │ │ │
│ 8. Decrypt │←────│ 7. Callback │←────│ │
│ (only you)│ │ result │ │ │
│ │ │ │ │ │
│ 9. Withdraw: │ │ Tokens sent │ │ │
│ receive │←────│ to wallet │ │ │
└──────────────┘ └──────────────┘ └──────────────┘

What happens at each step:

  1. Generate FHE keys — You create keys locally. Your secret key (clientKey) never leaves your machine.
  2. Register keys — ServerKey and PublicKey are registered with the coprocessor.
  3. Deploy Vault — You deploy your ExampleTokenVault contract to an EVM chain.
  4. Encrypt operation — You encrypt the operation details (transfer or withdraw). No one sees the plaintext.
  5. Submit operation — Your vault contract sends the encrypted request to the coprocessor.
  6. Compute on FHE — The coprocessor does math on encrypted data. It updates balances without knowing what they are.
  7. Callback result — New encrypted result CID is written back to your vault contract.
  8. Decrypt — Only you can decrypt and see the actual balances.
  9. Withdraw — For withdraw operations, tokens are transferred from the vault to your wallet.

Zero-Trust Security

Your keys never leave your machine:

ComponentWhere It LivesWho Can Access
ClientKeyYour machine onlyOnly you
ServerKeyZKSDK CoprocessorCan compute, cannot decrypt
PublicKeyZKSDK CoprocessorAnyone can encrypt to vault

The coprocessor mathematically cannot decrypt your data. It only has evaluation keys that can perform operations on ciphertext.


Quick Start

Prerequisites

  • Node.js 18+
  • Testnet ETH (get from Sepolia faucet)
  • Private key with testnet ETH

Install Dependencies

# Clone the example repo
git clone https://github.com/zksdk-labs/example-evm-fhe-slot-vault
cd example-evm-fhe-slot-vault

# Install
npm install

# Configure environment
cp .env.example .env
# Add your PRIVATE_KEY to .env

Run the Full Demo

npm run demo

This will:

  1. Generate FHE keys locally
  2. Deploy MockToken and ExampleTokenVault contracts
  3. Encrypt a private transfer
  4. Submit on-chain
  5. Simulate oracle processing
  6. Show decrypted result
  7. Withdraw tokens back to your wallet

Step-by-Step Guide

1. Generate FHE Keys

Generate FHE keys locally and register them with the coprocessor:

import { SlotVaultClient } from "@zksdk/js-private-token-slots-fhe";

const client = new SlotVaultClient({
workerUrl: "https://alpha.coprocessor.zksdk.com",
});

// Generate keys locally (takes ~30 seconds)
const { vaultKeyId, clientKey, serverKey, publicKey } =
await client.createVault();

// IMPORTANT: Save your clientKey securely - it's YOUR SECRET
// The coprocessor CANNOT decrypt without it
console.log("Vault Key ID:", vaultKeyId);
console.log("ClientKey size:", (clientKey.length / 1024).toFixed(1), "KB");
# Or run via npm script
npm run 1:keys

2. Deploy ExampleTokenVault Contract

Deploy your token and vault contracts to an EVM chain:

import { ethers } from "hardhat";

const COPROCESSOR_ADDRESS = "0x57936FF1f40BeE026dE56a45921E4084207F98A0";

// Deploy MockToken first
const MockToken = await ethers.getContractFactory("MockToken");
const mockToken = await MockToken.deploy();
const tokenAddress = await mockToken.getAddress();

// Mint some tokens
await mockToken.mint(deployer.address, ethers.parseEther("10000"));

// Deploy ExampleTokenVault with all constructor args
const ExampleTokenVault = await ethers.getContractFactory("ExampleTokenVault");
const vault = await ExampleTokenVault.deploy(
COPROCESSOR_ADDRESS,
tokenAddress,
vaultKeyId
);
await vault.waitForDeployment();

console.log("Vault deployed to:", await vault.getAddress());
# Or run via npm script
npm run 2:deploy

Note: ExampleTokenVault takes the vaultKeyId in its constructor, so no separate initialization step is needed.

4. Encrypt and Submit Private Transfer

Encrypt transfer parameters and submit on-chain:

// Load your vault
await client.loadVault(vaultKeyId, clientKey);

// Encrypt transfer (sender slot, receiver slot, amount)
const senderSlot = 0;
const receiverSlot = 1;
const amount = 50;
const { encrypted_data } = client.encryptTransfer(
senderSlot,
receiverSlot,
amount
);

// Store encrypted data and get CID
const cid = await client.storeBalanceBlob(encrypted_data);

// Submit to vault contract
const dummyReceiver = "0x0000000000000000000000000000000000000001";
const tx = await vault.privateTransfer(dummyReceiver, cid);
await tx.wait();

console.log("Transfer submitted! TX:", tx.hash);
npm run 4:transfer

5. Check Result

Poll for the oracle to process the request:

const vaultKeyId = await vault.vaultKeyId();
const balanceBlobCid = await vault.balanceBlobCid();
const token = await vault.token();

console.log("Vault Key ID:", vaultKeyId);
console.log("Balance Blob CID:", balanceBlobCid);
console.log("Token:", token);
npm run 5:result

6. Decrypt Result

Only you can decrypt the result using your clientKey:

// Fetch encrypted result
const encryptedResult = await client.fetchBalanceBlob(resultCid);

// Decrypt with your clientKey
const { balances, isValid } = client.decryptResult(encryptedResult);

console.log("Transfer valid:", isValid);
console.log("Slot 0 balance:", balances[0]);
console.log("Slot 1 balance:", balances[1]);
npm run 6:decrypt

7. Withdraw Tokens

Withdraw tokens from your private vault slot back to your wallet:

// Encrypt withdraw (slot index, amount)
const slotIndex = 0;
const withdrawAmount = 25;
const { encrypted_data } = client.encryptWithdraw(slotIndex, withdrawAmount);

// Store encrypted data and get CID
const withdrawCid = await client.storeBalanceBlob(encrypted_data);

// Submit withdraw to vault contract
const amountInUnits = ethers.parseUnits(withdrawAmount.toString(), 18);
const tx = await vault.withdraw(amountInUnits, withdrawCid);
await tx.wait();

console.log("Withdraw submitted! TX:", tx.hash);
npm run 7:withdraw

Contract: ExampleTokenVault

Your vault contract inherits from PrivateTokenSlotsClient:

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

import "./PrivateTokenSlotsClient.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract ExampleTokenVault is PrivateTokenSlotsClient {
using SafeERC20 for IERC20;

IERC20 public immutable token;
string public vaultKeyId;
string public balanceBlobCid;

constructor(address coProcessor, address tokenAddress, string memory _vaultKeyId)
PrivateTokenSlotsClient(coProcessor)
{
token = IERC20(tokenAddress);
vaultKeyId = _vaultKeyId;
}

function deposit(uint256 amount, uint8 positionIndex) external returns (bytes32) {
token.safeTransferFrom(msg.sender, address(this), amount);
return _submitDeposit(vaultKeyId, positionIndex, amount);
}

function privateTransfer(address receiver, string calldata encryptedDataCid) external returns (bytes32) {
address[] memory viewers = new address[](0);
return _submitPrivateTransfer("private_token_slots_v1", encryptedDataCid, bytes32(0), viewers);
}

function withdraw(uint256 amount, string calldata encryptedDataCid) external returns (bytes32) {
return _submitWithdraw("private_token_slots_v1", encryptedDataCid, amount);
}

function _onResult(bytes32 operationId, string calldata resultCid) internal override {
balanceBlobCid = resultCid;
// Handle withdrawals: transfer tokens to user
}
}

Network Configuration

ResourceValue
NetworkSepolia Testnet
CoProcessor Contract0x57936FF1f40BeE026dE56a45921E4084207F98A0
FHE Worker URLhttps://alpha.coprocessor.zksdk.com

Supported Networks

NetworkRPC URLStatus
Sepoliahttps://eth-sepolia.g.alchemy.com/v2/Active
Arbitrum Sepoliahttps://sepolia-rollup.arbitrum.io/rpcComing Soon
Base Sepoliahttps://sepolia.base.orgComing Soon
Optimism Sepoliahttps://sepolia.optimism.ioComing Soon

Storage

Encrypted data is stored off-chain. Configure storage in your SDK setup:

const client = new SlotVaultClient({
workerUrl: "https://alpha.coprocessor.zksdk.com",
storageType: "postgres", // Current default
});
TypeStatus
postgresAvailable
celestia, avail, eigenda, ipfsComing Soon

See full Storage documentation for all options and configuration details.


Files Created

After running the demo:

keys/
├── client.key # YOUR SECRET - backup this!
├── vault-key-id.txt # Vault identifier (64-char hex)
├── vault-address.txt # Deployed contract address
└── last-request-cid.txt # Last submitted request

Packages

PackagePlatformVersion
@zksdk/corenpm0.1.7
@zksdk/js-private-token-slots-fhenpm0.1.8
@zksdk/evm-corenpm0.1.1

Limitations (Alpha)

Current Limitations
  • Slot balances: 0-255 per slot (8-bit)
  • Slots per vault: Up to 256
  • Not audited: Do not use with real value

Troubleshooting

"insufficient funds"

Get testnet ETH:

"COPROCESSOR_ADDRESS not set"

Copy .env.example to .env:

cp .env.example .env

Contract verification

Verify on Etherscan:

npx hardhat verify --network sepolia <VAULT_ADDRESS> <COPROCESSOR_ADDRESS>

Support