The canonical interface for the Zama Wrappers Registry.
Browse, wrap, unwrap, and decrypt ERC-7984 confidential tokens with zero friction.
Built for the Zama Developer Program β Season 3 Β· Wrappers Registry Bounty Track
- Overview
- Live Deployment
- Product Demo
- Features
- Architecture
- How the Registry is Sourced
- How to Add a New Pair
- Official Sepolia cTokenMocks
- Technical Deep Dive
- Tech Stack
- Local Development
- Deployment
- Environment Variables
- Bounty Submission Checklist
- Verified On-Chain Evidence
- Known Limitations
Today, many developers spin up their own ERC-20 testnet tokens and ERC-7984 wrappers instead of using the ones that already exist in the official Zama Wrappers Registry. This fragments the ecosystem β every team ships against a slightly different set of tokens, integrations don't compose, and users end up with a wallet full of look-alike confidential assets that don't actually interoperate.
Macetz solves this by turning the official onchain registry into a complete, production-ready dApp. Every canonical ERC-20 β ERC-7984 pair is easy to find, wrap, unwrap, decrypt, and extend β making the official registry the path of least resistance.
| Feature | Macetz | Typical DIY dApp |
|---|---|---|
| Registry source | β Onchain canonical registry | β Hardcoded / local only |
| Dual-network support | β Sepolia + Ethereum Mainnet | β Sepolia-only |
| Pair extensibility | β Hybrid: onchain + local config | β Redeployment required |
| EIP-712 decrypt | β Any ERC-7984, not just registry pairs | β Registry tokens only |
| cTokenMock faucet | β All 7 official Sepolia mocks (public mint) | β Limited / none |
| Registry integrity checks | β Auto-flags anomalies per pair | β Blind render |
| Error handling | β User-friendly, context-aware messages | β Raw contract reverts |
| UX | β Premium, step-by-step flows | β Minimal |
| Environment | URL | Network |
|---|---|---|
| Production | https://macetz.vercel.app | Sepolia + Mainnet |
| App (dApp) | https://macetz.vercel.app/app | Sepolia + Mainnet |
| Local dev | npm run dev β http://localhost:3000/app |
Sepolia (recommended for judges) |
Note for Judges: Connect MetaMask to Sepolia for the full bounty flow (registry β faucet β shield β decrypt β unshield β arbitrary ERC-7984 decrypt). Ethereum mainnet is supported for browse and relayer-dependent shield/decrypt with a real-funds confirmation gate. Confidential Distribute (TokenOps) is Sepolia-only β the official disperse singleton is deployed on testnet.
The app is fully self-contained β it can also be run locally in one command (
npm run dev) or redeployed anywhere with the includedvercel.json.
The complete bounty flow, captured live on Sepolia. Every GIF below maps 1:1 to a hard requirement of the Wrappers Registry bounty.
Live read of getTokenConfidentialTokenPairs() on Sepolia: β Docs-verified badges, integrity checks, Dev-pair labels, and one-click network switch to Mainnet and back.
Mint All mints every public cTokenMock (cUSDC, cUSDT, cWETH, cBRON, cZAMA, ctGBP, cXAUt mocks) straight from the official Sepolia contracts.
Auto allowance detection, then a single Approve & Wrap with the live step indicator (Approving β Wrapping β Confirmed).
The encrypted euint64 balance becomes readable after a single EIP-712 signature. The signature can only ever decrypt the signer's own balance.
unwrap() β Zama relayer decrypts β finalizeUnwrap(). The UI shows a real Pending Finalization state (~30β90s) instead of faking instant success.
Paste an arbitrary wrapper address (here: our own cMTUSD, deployed via the dev-guide and not in the official registry) β validated onchain via ERC-165 β decrypted.
The in-app Admin UI validates a new pair onchain (ERC-165 + decimals), previews it instantly, and emits a copy-paste custom-pairs.json snippet.
CSV of recipients in, one TokenOps disperse tx out. Recipients are visible onchain; amounts never are. Each recipient decrypts only their own slice.
π₯ Full narrated demo video: [VIDEO_LINK β add before submitting]
Macetz is the first Wrappers Registry interface to explicitly support both networks listed in Zama's official documentation.
| Chain | Registry Address | Wrapper Pairs | Browse | Decrypt | Wrap/Unwrap | Faucet | Distribute |
|---|---|---|---|---|---|---|---|
| Sepolia | 0x2f0750Bb...128e |
8 docs-listed (7 mocks + ctGBP) | β | β | β | β | β (TokenOps, docs-verified only) |
| Ethereum Mainnet | 0xeb5015fF...bBA0 |
9 docs-listed | β | β | β |
- A network switch control in the sidebar shows the active network with an animated indicator and lets you switch chains with a single click β this triggers a real MetaMask chain-switch request, not a UI state toggle.
- The Faucet nav item animates in/out (height + opacity, 0.2s) reactively based on the actual connected
chainIdβ visible on Sepolia, removed from the DOM on Mainnet. - Switching networks automatically reloads the registry from the correct contract address.
Wrap and unwrap on Ethereum mainnet show an explicit confirmation dialog before any transaction: "You are about to shield/unshield [amount] [token] on Ethereum mainnet β this uses real funds and cannot be undone." The user must click "I understand, proceed" to continue. This is exactly the kind of production-conscious UX that "could a real user trust it today" implies.
Mainnet FHE caveat: Wrap/decrypt on mainnet depend on Zama's mainnet relayer being provisioned (NEXT_PUBLIC_MAINNET_RELAYER_URL). Registry browsing works fully; FHE operations may return 403 until the relayer is live.
Macetz automatically flags registry anomalies rather than blindly rendering every entry β a feature no known competitor has shipped as a live product feature.
| Check | Pass | Flag |
|---|---|---|
| Wrapper decimals | β€ 6 (per Zama ERC-7984 spec) | > 6 decimals |
| Underlying address | Valid non-zero address | Zero address |
| Duplicate detection | Official + Mock split is expected | Two+ non-Mock-distinguishable entries share a base symbol |
Some symbols legitimately have two entries: an official production wrapper (e.g., ctGBP) AND a separate Mock testnet-only wrapper (ctGBPMock). Macetz correctly identifies this as intentional design and does NOT flag it as a duplicate β only genuinely suspicious duplicates (same base symbol, no official/Mock relationship) are flagged.
- Each pair in the Registry Browser shows a
β Verifiedorβ Flaggedintegrity badge. - Registry pairs also show
β Docs-verified(wrapper in Zama's official docs table) or a neutralRegistrybadge with a caution note when listed onchain but not in the docs allowlist. - Flagged pairs remain fully functional for Shield/Decrypt when
isValidβ not hidden, just clearly marked. - The Registry header shows a count of flagged pairs and a legend explaining the checks.
- Displays every official ERC-20 β ERC-7984 wrapper pair from the onchain Zama Wrappers Registry
- Shows token metadata: symbol, name, decimals, and both contract addresses with Etherscan links
- Network-aware: auto-switches between Sepolia and Mainnet registry contracts based on wallet connection
- Integrity badges: each pair shows
β Verifiedorβ Flaggedbased on automatic anomaly checks - Clearly labels mock (testnet-only) vs. production tokens
- Merges local dev-only pairs with a "Dev Pair" badge β
configExampleentries are registry display only (not offered in Shield/Decrypt/Distribute until real contracts are deployed)
- Sidebar Docs tab with table of contents, copyable code blocks, and the full
dev-guide/deploy walkthrough - Structured content in
src/lib/docs-content.tsβ edit docs without touching layout code
- 4-step sender wizard: token β recipients (CSV) β preflight review β disperse tx
- Recipient view: detect pending allocations β EIP-712 decrypt own payroll amount
- Uses official TokenOps singleton
0x710dD9885Cc9986EfD234E7719483147a6d8DBb4on Sepolia
- Auto-detects current ERC-20 allowance before submitting
- One-click "Approve & Wrap" or a clean two-step flow for transparency
- Shows real-time step indicator:
Approving β Wrapping β Confirmed - Mainnet safety: explicit real-funds confirmation dialog before any mainnet transaction
- Displays updated ERC-7984 balance immediately after wrap
- Full two-step async unwrap:
unwrap()β relayer decryption βfinalizeUnwrap() - Shows a "Pending Finalization" state with timer and human-readable explanation
- Tracks unwrap request IDs onchain for auditability
- Mainnet safety: explicit real-funds confirmation dialog before any mainnet transaction
- Decrypts any ERC-7984 balance via EIP-712 user-decryption
- Works for any ERC-7984 contract address β not only registry pairs
- Works on both networks: decrypt is read-only/EIP-712 and fully supported on Ethereum mainnet
- Two modes:
- Registry mode: Select from the official pair dropdown
- Universal mode: Paste any address β validated via
supportsInterface(0x4958f2a4)before attempting decrypt
- Claims official cTokenMock test tokens (up to 1,000 per mint call)
- Covers all 7 public-mint Sepolia cTokenMocks listed in the Zama docs
- One-click mint with real-time transaction status feedback
- Mainnet-aware: Faucet nav item is removed from sidebar on Mainnet (animates out with height + opacity transition); navigating to faucet on Mainnet shows an informational banner instead
graph TD
A["User / Browser"] --> B["Next.js 15 App Router"]
B --> C["Landing Page /"]
B --> D["dApp /app"]
D --> E["Web3Provider"]
E --> F["wagmi v3"]
E --> G["@zama-fhe/react-sdk"]
D --> H["Registry Tab"]
D --> I["Wrap/Unwrap Tab"]
D --> J["Decrypt Tab"]
D --> K["Faucet Tab\n(Sepolia only)"]
D --> L2["Distribute Tab\n(TokenOps Sepolia)"]
D --> L3["Docs Tab"]
H --> L["useRegistryPairs hook"]
L --> M["fetchRegistryPairs()"]
M --> N["Onchain Registry Contract\n0x2f0750Bb...128e"]
M --> O["config/custom-pairs.json\nLocal Dev Pairs"]
I --> P["useShield hook\n@zama-fhe/react-sdk"]
I --> Q["useUnshield hook\n@zama-fhe/react-sdk"]
J --> R["useConfidentialBalance hook\n@zama-fhe/react-sdk"]
K --> S["useFaucet hook\nmint ERC-20 directly"]
G --> T["Relayer Proxy (default)\n{origin}/api/relayer/{chainId}"]
T --> U["Zama Public Relayer\n(testnet or mainnet)"]
macetz/
βββ src/
β βββ app/
β β βββ page.tsx # Landing page
β β βββ app/page.tsx # dApp shell (Registry/Shield/Decrypt/Faucet/Distribute/Docs)
β β βββ api/relayer/[...path]/route.ts # Relayer proxy (Sepolia + Mainnet)
β βββ components/app/
β β βββ RegistryBrowser.tsx # Browse pairs + Add a Pair admin UI
β β βββ WrapUnwrapPanel.tsx # Shield / Unshield
β β βββ DecryptPanel.tsx # Registry + Any ERC-7984 decrypt
β β βββ FaucetPanel.tsx # cTokenMock mints (Sepolia)
β β βββ DistributePanel.tsx # TokenOps payroll wizard (Sepolia)
β β βββ DocsPanel.tsx # In-app documentation
β βββ hooks/
β β βββ useRegistryPairs.ts # Onchain + custom + preview merge
β β βββ useFaucet.ts
β βββ lib/
β β βββ config.ts # KNOWN_MOCK_PAIRS, registry addresses
β β βββ registry.ts # Onchain fetch + custom-pairs loader
β β βββ pair-utils.ts # Operational-pair gate (Shield/Decrypt/Distribute)
β β βββ pair-validation.ts # Add Pair on-chain validation
β β βββ preview-pairs.ts # Browser localStorage previews
β β βββ disperse.ts # TokenOps campaign helpers
β β βββ docs-content.ts # In-app docs (structured data)
β βββ providers/Web3Provider.tsx # wagmi + Zama SDK (shared config)
βββ config/custom-pairs.json # Chain-keyed dev pairs (`configExample` = display-only)
βββ dev-guide/ # Hardhat deploy your own pair
βββ scripts/verify-distribute.ts # Autonomous registry + Distribute smoke tests
βββ vercel.json
sequenceDiagram
participant U as User
participant A as App (Next.js)
participant R as Registry Contract
participant C as custom-pairs.json
participant Z as Zama Relayer
participant BC as Blockchain (Sepolia)
Note over A: App Initialization
A->>R: getTokenConfidentialTokenPairs()
R-->>A: [{tokenAddress, confidentialTokenAddress, isValid}]
A->>C: Load config/custom-pairs.json
C-->>A: [{erc20, erc7984, symbol, decimals}]
A->>A: filter(isValid) + merge + deduplicate
A-->>U: Display unified registry
Note over U,BC: Wrap Flow
U->>A: Select pair + enter amount
A->>BC: ERC20.allowance(user, wrapper)
BC-->>A: currentAllowance
alt Insufficient Allowance
A->>BC: ERC20.approve(wrapper, amount)
BC-->>A: tx confirmed
end
A->>Z: shield(wrapper, amount) via Zama SDK
Z->>BC: wrapper.wrap(encryptedAmount)
BC-->>A: ERC-7984 minted event
A-->>U: Confirmed
Note over U,BC: Decrypt Flow
U->>A: Select or paste ERC-7984 address
A->>BC: supportsInterface(0x4958f2a4)
BC-->>A: isERC7984 = true
A->>Z: requestUserDecrypt(addr, EIP-712 sig)
Z->>BC: FHE.decrypt(encryptedBalance)
BC-->>Z: plaintext balance
Z-->>A: plaintext + proof
A-->>U: Display decrypted balance
Macetz uses a hybrid registry model β the onchain registry is always the authoritative source, while a local config allows dev-only extensions without fragmenting the canonical set.
// src/lib/registry.ts
export async function fetchRegistryPairs(
client: PublicClient,
chainId: number
): Promise<TokenPair[]> {
const registryAddress = getRegistryAddress(chainId);
const officialAddresses = getOfficialAddresses(chainId);
const rawPairs = await client.readContract({
address: registryAddress,
abi: REGISTRY_ABI,
functionName: "getTokenConfidentialTokenPairs",
});
// All valid onchain pairs β registry is source of truth
const validPairs = rawPairs.filter((p) => p.isValid);
const pairs = await Promise.all(
validPairs.map(async (raw) => {
const [erc20Meta, erc7984Meta] = await Promise.all([
fetchTokenMetadata(client, raw.tokenAddress),
fetchTokenMetadata(client, raw.confidentialTokenAddress),
]);
const wrapperInDocs = officialAddresses.has(
raw.confidentialTokenAddress.toLowerCase()
);
return {
erc20Address: raw.tokenAddress,
erc7984Address: raw.confidentialTokenAddress,
// ...metadata fields...
source: "registry" as const,
isValid: true,
docsVerified: wrapperInDocs, // badge only β does not filter pairs out
};
})
);
return runIntegrityChecks(pairs);
}// src/lib/registry.ts β chain-keyed local config
export function loadCustomPairs(chainId: number): TokenPair[] {
const config = customPairsJson as CustomPairsConfig;
const entries = config[String(chainId)] ?? [];
return entries.map((entry) => mapCustomEntryToPair(entry, "local-dev"));
}Custom pairs are tagged source: "local-dev". Entries with "configExample": true (seeded cDEMO1 / cDEMO2) appear in the Registry tab only β they are excluded from Shield, Decrypt, and Distribute until replaced with deployed contract addresses.
flowchart LR
A["Onchain Registry\ngetTokenConfidentialTokenPairs()"] --> C["filter(isValid === true)"]
B["config/custom-pairs.json\nsource: local-dev"] --> D["Tag as Dev Pair"]
C --> E["Merge + Deduplicate\n(onchain > custom > preview)"]
D --> E
E --> F["Render Unified Registry UI\n+ docsVerified badge"]
Macetz supports four paths for adding pairs:
Submit the pair to the onchain Zama Wrappers Registry. Once registered and isValid == true, Macetz will automatically surface it on next load β zero code changes required.
ConfidentialTokenWrappersRegistry (Sepolia)
0x2f0750Bbb0A246059d80e94c454586a7F27a128e
Follow the Zama Wrappers Registry documentation for the registration process.
Ideal for developers iterating on new ERC-7984 tokens before official registration.
Live reference: open the Registry tab on Sepolia β you'll see pre-seeded config examples tagged Dev (cDEMO1, cDEMO2). These illustrate the JSON schema only; they are not deployed contracts and cannot be wrapped or decrypted.
Step 1: Open config/custom-pairs.json
Step 2: Add your entry under the correct chain key ("11155111" for Sepolia, "1" for Mainnet):
{
"11155111": [
{
"erc20": "0xYourUnderlyingERC20Address",
"erc7984": "0xYourERC7984WrapperAddress",
"symbol": "cMYTOKEN",
"decimals": 18,
"source": "local-dev"
}
],
"1": []
}| Field | Type | Required | Description |
|---|---|---|---|
erc20 |
address |
β | Underlying ERC-20 address on the target network |
erc7984 |
address |
β | ERC-7984 wrapper address on the target network |
symbol |
string |
β | Ticker for the wrapper token (prefix with c) |
decimals |
number |
β | ERC-20 decimals (max 18; wrapper auto-caps at 6) |
source |
"local-dev" |
β | Must always be "local-dev" |
configExample |
boolean |
Optional | When true, pair is registry display-only (no Shield/Decrypt/Distribute) |
Step 3: Restart dev server. Pair shows with Dev badge.
- Open Registry β Add a Pair
- Paste ERC-20 + ERC-7984 addresses (validated on the currently connected network)
- Pair appears instantly as Preview β browser-only, chain-scoped in
localStorage - Click Copy Config Snippet to paste into
custom-pairs.jsonfor a permanent entry
Works on both Sepolia and Mainnet β preview pairs never leak across networks.
Cross-platform Hardhat guide (macOS + Windows): dev-guide/README.md.
npm run deploy:sepolia β paste addresses into Path B or Path C.
Security note: Local-dev and preview pairs are visually separated from canonical on-chain registry pairs.
All 8 official Sepolia confidential wrappers from the Zama docs are hardcoded in src/lib/config.ts (7 mocks + ctGBP). The Faucet tab shows the 7 public-mint mocks only:
| Symbol | ERC-7984 Wrapper | Underlying ERC-20 | Faucet |
|---|---|---|---|
| cUSDCMock | 0x7c5BF43...3639 | 0x9b5Cd1...fF | β Public |
| cUSDTMock | 0x4E7B06...491 | 0xa7dA08...b0 | β Public |
| cWETHMock | 0x46208...158 | 0xff5473...3F | β Public |
| cBRONMock | 0xaa5612...891 | 0xFf021f...5E | β Public |
| cZAMAMock | 0xf2D628...bFB | 0x75355a...57 | β Public |
| ctGBPMock | 0xfCE5c7...7CC | 0x93c931...42 | β Public |
| cXAUtMock | 0xe4FcF8...0C7 | 0x24377A...40 | β Public |
| ctGBP | 0x167DC9...208 | 0xf6Ef9A...f3 |
stateDiagram-v2
[*] --> Idle : User enters amount
Idle --> CheckingAllowance : Submit clicked
CheckingAllowance --> NeedsApproval : allowance < amount
CheckingAllowance --> Wrapping : allowance >= amount
NeedsApproval --> Approving : User confirms approve tx
Approving --> Wrapping : Approval confirmed on-chain
Wrapping --> Confirmed : Wrap tx mined + ERC-7984 minted
Wrapping --> Error : Tx rejected or reverted
Approving --> Error : Approval rejected
Confirmed --> [*]
Error --> [*] : User retries
Key implementation details:
- Allowance check β
ERC20.allowance(user, wrapperAddress)viaviem readContract - ERC-20 Approve β
ERC20.approve(wrapperAddress, rawAmount)standard tx - Shield / Wrap β
useShield()from@zama-fhe/react-sdkencrypts the amount client-side with TFHE and callswrapper.wrap(encryptedInput)via the Zama SDK - Rate conversion β
wrapper.rate()converts between ERC-20 raw units and the 6-decimal encrypted ERC-7984 representation
Unwrapping requires the Zama relayer to publicly decrypt the amount before the ERC-20 can be transferred back:
sequenceDiagram
participant U as User
participant A as App
participant W as Wrapper Contract
participant R as Zama Relayer
participant BC as Blockchain
U->>A: Click Unwrap + confirm amount
A->>W: unwrap(encryptedAmount)
W->>BC: emit UnwrapRequested(requestId, receiver, encAmt)
BC-->>A: tx confirmed
A-->>U: Show Pending Finalization state
Note over R: Relayer monitors UnwrapRequested events
R->>BC: FHE.decrypt(encAmt)
BC-->>R: plaintext amount
R->>W: finalizeUnwrap(requestId, plainAmt)
W->>BC: ERC20.transfer(receiver, plainAmt)
BC-->>A: UnwrapFinalized event
A-->>U: Confirmed β ERC-20 received
The UI shows a two-phase state machine: requesting β pending-finalization (~30β90s) β confirmed, with a clear explanation of why finalization takes time.
sequenceDiagram
participant U as User
participant A as App
participant W as Wallet (MetaMask)
participant Z as Zama Relayer
participant C as ERC-7984 Contract
U->>A: Paste address or select from dropdown
A->>C: supportsInterface(0x4958f2a4)
C-->>A: isERC7984 = true
A->>W: eth_signTypedData_v4 (EIP-712 permit)
Note over W: Domain: ERC7984UserDecrypt, chainId: 11155111
W-->>U: Signature request prompt
U->>W: Sign
W-->>A: ECDSA signature
A->>Z: userDecrypt(contractAddr, signature, userPubKey)
Z->>C: getEncryptedBalance(user)
C-->>Z: ciphertext balance (euint64)
Z->>Z: TFHE.decrypt(ciphertext)
Z-->>A: plaintext balance
A-->>U: Display Your Balance: X.XXXXX cTOKEN
The EIP-712 signature authorizes the relayer to decrypt only the signing user's balance β it is impossible to decrypt another user's balance with this flow.
Every wallet interaction routes through a centralized error formatter in src/lib/errors.ts:
export function formatWalletError(error: unknown, chainId?: number): string {
const msg = (error instanceof Error ? error.message : String(error)).toLowerCase();
if (msg.includes("user rejected") || msg.includes("user denied"))
return "Transaction rejected by user.";
if (msg.includes("wrong network") || msg.includes("chain mismatch"))
return chainId === 1
? "Please switch to Ethereum mainnet to continue."
: "Please switch to Sepolia testnet to continue.";
if (msg.includes("gas"))
return chainId === 1
? "Transaction requires more gas. Ensure you have enough ETH."
: "Transaction requires more gas. Ensure you have enough Sepolia ETH.";
// ...additional mappings...
}| Failure Scenario | User-Facing Message |
|---|---|
| User cancels MetaMask prompt | "Transaction rejected by user." |
| Insufficient ERC-20 balance | "Insufficient balance for this transaction." |
| Missing ERC-20 allowance | "Token approval required. Please approve first." |
| Wrong network connected | "Please switch to Sepolia testnet to continue." |
| Not an ERC-7984 address | "This address does not support ERC-7984 decryption." |
| Gas estimation failure | "Ensure you have enough Sepolia ETH." |
| Contract reverts | "The contract rejected this operation." |
| Relayer timeout | "Transaction timed out. Please check your wallet." |
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Framework | Next.js | 15 (App Router) | SSR, API routes, file routing |
| Language | TypeScript | 5.x (strict) | Full type safety across all modules |
| Styling | Tailwind CSS | v4 | Utility-first, responsive styling |
| Animation | Motion (Framer Motion) | v12 | Scroll reveals, micro-interactions |
| Web3 State | wagmi | v3 | Wallet connection, tx lifecycle |
| EVM Client | viem | v2 | Type-safe contract reads + writes |
| FHE React SDK | @zama-fhe/react-sdk | v3 | useShield, useUnshield, useConfidentialBalance |
| FHE Core | @zama-fhe/sdk | v3 | TFHE encryption primitives |
| Data Fetching | TanStack React Query | v5 | Server state, smart caching |
| Wallet Support | @wagmi/connectors | v8 | MetaMask, WalletConnect v2 |
- Node.js β₯ 20.x
- A wallet with Sepolia ETH (faucet: https://sepoliafaucet.com)
- Sepolia RPC URL (Alchemy, Infura, or use the default public node)
# 1. Clone the repository
git clone https://github.com/pramadanif/macetz.git
cd macetz
# 2. Install dependencies
# (the repo-level .npmrc sets legacy-peer-deps for the TokenOps SDK,
# so a plain npm install works out of the box)
npm install
# 3. Configure environment variables
cp .env.example .env.local
# Edit .env.local (see Environment Variables section below)
# 4. Start the development server
npm run dev
# Landing: http://localhost:3000
# dApp: http://localhost:3000/app# TypeScript strict mode β zero type errors expected
npm run lintnpm run build
npm run start# One-command deploy
npx vercel --prodNo backend is required. All FHEVM operations run client-side via the Zama SDK. By default the client routes relayer traffic through the same-origin Next.js proxy at /api/relayer/<chainId> (built from window.location.origin). Override with NEXT_PUBLIC_RELAYER_URL / NEXT_PUBLIC_MAINNET_RELAYER_URL to hit Zama relayers directly.
Set these in your Vercel project settings under Settings β Environment Variables:
NEXT_PUBLIC_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_wc_project_id
# Optional β unset uses same-origin proxy automatically in the browser
# NEXT_PUBLIC_RELAYER_URL=https://your-app.vercel.app/api/relayer/11155111
| Variable | Required | Default | Description |
|---|---|---|---|
NEXT_PUBLIC_RPC_URL |
Optional | Public Sepolia node | Sepolia JSON-RPC endpoint |
NEXT_PUBLIC_MAINNET_RPC_URL |
Optional | Public mainnet node | Mainnet JSON-RPC endpoint |
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID |
Optional | "" |
Enables WalletConnect modal |
NEXT_PUBLIC_RELAYER_URL |
Optional | Same-origin /api/relayer/11155111 in browser |
Override Sepolia relayer URL (absolute http(s)) |
NEXT_PUBLIC_MAINNET_RELAYER_URL |
Optional | Same-origin /api/relayer/1 proxy |
Override mainnet relayer URL (mainnet FHE ops are relayer-dependent) |
Copy .env.example to .env.local to get started:
# .env.example
# Sepolia RPC (Alchemy, Infura, or public node)
NEXT_PUBLIC_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
# WalletConnect Project ID β get one at cloud.walletconnect.com
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
# Sepolia relayer β unset = same-origin proxy in browser ({origin}/api/relayer/11155111)
# NEXT_PUBLIC_RELAYER_URL=https://your-app.vercel.app/api/relayer/11155111
# Mainnet relayer β unset = same-origin proxy; upstream may 403 until Zama provisions mainnet
# NEXT_PUBLIC_MAINNET_RELAYER_URL=https://your-app.vercel.app/api/relayer/1Macetz integrates TokenOps Confidential Disperse (@tokenops/sdk/fhe-disperse) in the same app as the Wrappers Registry bounty β no separate deployment.
Corporate payroll on Sepolia: HR shields USDC (or any registry mock), batches encrypted salaries to employee wallets in one transaction. Each employee decrypts only their own allocation via EIP-712. Third parties see recipients but not individual amounts.
There is no custom disperse contract in this repo. The app calls the official TokenOps singleton via SDK:
| Network | DisperseConfidential Singleton |
|---|---|
| Sepolia | 0x710dD9885Cc9986EfD234E7719483147a6d8DBb4 |
Campaign clones are not used for disperse β the singleton + SDK useDisperse handles encryption, ACL grants, and confidentialTransferFrom in one tx (mode: "direct").
- Sender wizard (4 steps): Select docs-verified operational shielded token β Review preflight (
usePreflightDisperse) β Approve singleton operator +useDispersetx β Track claim status (pending/claimed viaconfidentialBalanceOfβ amounts never shown to third parties) - Recipient view: Scan operational registry tokens for pending handles β Decrypt & Claim via EIP-712
- CSV upload:
address,amountper line for payroll imports - Mainnet: informational banner β disperse singleton is Sepolia-only
Payroll safety: Distribute (isDistributeOperationalPair) requires docsVerified === true for onchain registry pairs. Non-docs onchain pairs remain usable in Shield/Decrypt.
Autonomous smoke tests: npm run verify:distribute
npm install @tokenops/sdk --legacy-peer-depsEncryptor uses the live Zama relayer from @zama-fhe/react-sdk (useZamaSDK().relayer), forwarded lazily into useDisperse({ encryptor: () => relayer }) per TokenOps docs.
| Requirement | Status | Where |
|---|---|---|
| Public GitHub repo | β | This repository |
| Live URL (wallet connect) | β https://macetz.vercel.app | Live Deployment |
| Sepolia: browse registry | β | Registry tab β all valid onchain pairs + docs-verified badge |
| Sepolia: all 7 cTokenMock faucet mints | β | Faucet tab β KNOWN_MOCK_PAIRS + Mint All |
| Sepolia: wrap / unwrap every registry pair | β | Shield tab β useShield / useUnshield |
| Sepolia: decrypt registry + arbitrary ERC-7984 | β | Decrypt tab β Registry + Any ERC-7984 modes |
| Hybrid registry (onchain + local config) | β | registry.ts + custom-pairs.json + Admin UI |
| Documented add-pair process (4 paths) | β | README + in-app Docs + dev-guide/ |
| EIP-712 user-decryption | β | @zama-fhe/react-sdk useConfidentialBalance |
| Relayer SDK integration | β | Web3Provider defaults to /api/relayer/<chainId> proxy |
| Error handling (approval, balance, network) | β | src/lib/errors.ts + UI guards |
| Demo video + X thread | π² Submitter delivers | β |
Judge quick path (Sepolia): Faucet mint cUSDCMock β Shield wrap β Decrypt balance β Unshield β Decrypt tab paste arbitrary wrapper from dev-guide deploy.
Run automated checks: npm run verify
Every hash below was produced by the reproducible E2E script scripts/sepolia-bounty-e2e.ts (run it with your own funded key to regenerate). The dev-guide deployment record lives in dev-guide/deployed-addresses.json.
| Action | Tx hash / address | Etherscan |
|---|---|---|
| Faucet mint (cUSDCMock) | 0x587c256bf57262a1f355aa724dcd1f115f0314a99c9954cc7a281b724c96da9b |
View |
| Wrap (shield cUSDCMock) | 0xa17bdde962fcddcc9a63310110714ad846d3e36982782740339a4e51fc0fd6ac |
View |
| Decrypt balance | EIP-712 relayer (no on-chain tx) β cUSDCMock + cMTUSD verified 2026-07-07 | β |
| Unwrap (unshield phase 1) | 0x8d90e5c42548484d9e55abe2c2be6b97b23973211402a902fe3f9e4f13e7069f |
View |
| Unwrap finalize (phase 2) | 0xc6101c96c5c6ed3751b6586cad8f848ea64c9d5934142e6959da3ffa42889d65 |
View |
| TokenOps disperse | 0x41d71ac3f1d8f701f9a85d8d0a3d4b58bae1ba238a6d0fae7ce1d16c3f74f9d1 |
View |
| dev-guide deploy (MTUSD) | ERC-20 0x022D67AeE3a5f841CC0c422F0B849B366f2c59B7 |
View |
| dev-guide deploy (cMTUSD) | Wrapper 0x3A1E3F5a8C5975078C587C73E80A916505538C4B |
View |
| dev-guide deployer | 0xB4d186aF4d691dE665a36BDA1104067e069a15F8 |
View |
| dev-guide wrap tx | 0xd767600c0d9e96f2c173d8c0b2596c57d5715c7ef31d685f969712303f52e976 |
View |
- Bounty E2E on Sepolia β Full wrap/unwrap/faucet/distribute flow is validated on Sepolia testnet. Mainnet supports registry browse; shield/decrypt are relayer-dependent and may fail until Zama provisions the mainnet relayer.
- Config examples β
configExample: truedev pairs are registry display-only until replaced with deployed addresses - Single-token batches β TokenOps Distribute processes one confidential token per payroll run
- Injected wallet β Requires MetaMask or any EIP-1193 wallet
- Relayer latency β Unwrap finalization depends on Zama's relayer (~30β90s on Sepolia)
- Unaudited β Community project for the Zama Developer Program; do not use with real funds