create-shield-unshield-dapp
v1.0.6
Published
Scaffold a Shield/Unshield dApp for USDC/cUSDC and USDT/cUSDT (wrap, unwrap, confidential transfer)
Maintainers
Readme
create-shield-unshield-dapp
Scaffold a Shield/Unshield dApp for the Zama ecosystem: let users wrap USDC/USDT into confidential tokens (cUSDC/cUSDT), send them privately, and unwrap back. Built on ERC-7984 (confidential token standard). Ideal for projects that want to support shield/unshield and confidential transfers, and for developers new to Zama who want a working reference with clear documentation.
What you get
- A ready-to-run React + Vite app with wallet connection (e.g. Rainbow, MetaMask).
- Wrap (shield): USDC → cUSDC, USDT → cUSDT.
- Unwrap (unshield): Two-step flow (unwrap → finalizeUnwrap) with Zama relayer for decryption.
- Confidential transfer: Send cUSDC/cUSDT with amounts hidden on-chain.
- A detailed README in the scaffolded project (env vars, build, where to get Sepolia addresses).
- A contracts/ folder with an ERC-7984 wrapper (same pattern as the payroll app), MockUSDC/MockUSDT, and Hardhat deploy scripts so you can deploy your own tokens and wrappers for local or Sepolia testing. See
contracts/README.mdafter scaffolding.
Token standard: ERC-7984
The scaffolded app uses ERC-7984 (Confidential Token Standard). In short:
- Underlying token: A normal ERC-20 (e.g. USDC, USDT).
- Confidential wrapper: A contract that holds the underlying and mints/burns confidential balances. On-chain amounts are encrypted (FHE); only the owner and authorized systems can decrypt with a proof.
- cUSDC / cUSDT: The confidential versions of USDC and USDT. Balances are stored as encrypted handles; you see your balance by decrypting (e.g. via Zama’s relayer/gateway).
Key contract functions used in the app:
| Function | Purpose |
|----------|---------|
| wrap(to, amount) | Lock underlying (e.g. USDC) in the contract and mint confidential balance for to. |
| unwrap(from, to, encryptedAmount, inputProof) | Burn confidential balance and request unwrap; the amount is still encrypted until proven. Emits UnwrapRequested. |
| finalizeUnwrap(burntAmount, burntAmountCleartext, decryptionProof) | Prove the burnt amount to the contract; contract releases that much underlying to the receiver. |
| confidentialTransfer(to, encryptedAmount, inputProof) | Transfer confidential balance to to; amount stays encrypted on-chain. |
| confidentialBalanceOf(account) | Returns an encrypted balance handle (decrypt off-chain to show amount). |
How wrapping works (shield)
Goal: Turn public USDC/USDT into confidential cUSDC/cUSDT.
- Approve: User approves the confidential token contract to spend their underlying (USDC or USDT).
- Wrap: User calls
wrap(to, amount)on the cUSDC/cUSDT contract. The contract:- Pulls
amountof underlying from the user (transferFrom). - Mints confidential balance for
to(the user). On-chain, the amount is stored in encrypted form.
- Pulls
- Balance: The user’s cUSDC/cUSDT balance increases (shown after decrypting the handle). Their USDC/USDT balance decreases.
No relayer needed for wrapping; it’s a single on-chain transaction.
How unwrapping works (unshield) — two steps
Goal: Turn confidential cUSDC/cUSDT back into public USDC/USDT.
Unwrap is two-step because the contract only holds encrypted amounts. To release the right amount of underlying, the contract must be given a decryption proof that matches the burnt confidential amount. The Zama relayer (gateway) produces that proof after the first step.
Step 1: unwrap(from, to, encryptedAmount, inputProof)
- User chooses amount and the app encrypts it (FHE) for the contract, producing an encrypted handle and
inputProof. - User sends a tx calling
unwrap(from, to, encryptedAmount, inputProof). The contract:- Burns the confidential balance corresponding to that encrypted amount.
- Emits
UnwrapRequested(receiver, amount)whereamountis the encrypted (handle) value. - Does not send underlying yet — it waits for a decryption proof.
- The app (or backend) sends the emitted handle to the Zama relayer. The relayer decrypts it and returns a decryption proof.
Step 2: finalizeUnwrap(burntAmount, burntAmountCleartext, decryptionProof)
- The app calls
finalizeUnwrap(burntAmount, burntAmountCleartext, decryptionProof)with:burntAmount: the handle fromUnwrapRequested.burntAmountCleartext: the decrypted amount (so the contract can release the right amount).decryptionProof: proof from the relayer that the decryption is correct.
- The contract verifies the proof and sends
burntAmountCleartextof underlying (USDC/USDT) to the receiver. - User’s public USDC/USDT balance increases.
In the scaffolded app, both steps run in one flow after the user clicks Unwrap/Unshield: we wait for the unwrap tx, get the proof from the relayer, then call finalizeUnwrap and wait for that receipt so the UI can refetch balances and show the updated USDC/USDT.
How confidential transfer works
Goal: Send cUSDC/cUSDT to another address without revealing the amount on-chain.
- Sender enters amount and recipient address.
- The app encrypts the amount (FHE) for the contract, producing an encrypted handle and
inputProof. - User sends a tx:
confidentialTransfer(to, encryptedAmount, inputProof). The contract deducts the encrypted amount from the sender and adds it to the recipient’s confidential balance. The amount stays encrypted; no relayer needed.
Install and run
npx create-shield-unshield-dapp my-app
cd my-app
cp .env.example .env
# Edit .env (see below and scaffolded README)
npm install
npm run devOpen the URL (e.g. http://localhost:5173) and connect your wallet to the correct network (Mainnet or Sepolia).
Mainnet vs testnet
- Mainnet: In
.envsetVITE_MAINNET=true. Contract addresses are built-in. Optionally setVITE_MAINNET_RPC_URL. - Testnet (Sepolia): Leave
VITE_MAINNET=falseor omit it. Set the four Sepolia contract addresses in.env:VITE_USDC_ADDRESS,VITE_CONF_USDC_ADDRESS,VITE_USDT_ADDRESS,VITE_CONF_USDT_ADDRESS.
The scaffolded project’s README (README.md in the created folder) has full env docs, where to get Sepolia addresses, build, and preview.
Options
npx create-shield-unshield-dapp my-app --force— Overwrite existing files inmy-appif it already exists.npx create-shield-unshield-dapp my-app --no-install— Skipnpm installafter copying (run it yourself).
License
MIT.
