Shield & Unshield
Shield and unshield are the entry and exit points between public and private balances.
Overview
┌─────────────────────────────────────────────────────────────┐
│ EMT Token │
├─────────────────────────────────────────────────────────────┤
│ │
│ Public Balance Encrypted Balance │
│ balanceOf(user) encryptedBalances[bpk] │
│ │
│ │ ▲ │
│ │ │ │
│ │ ──────── shield() ────────────► │ │
│ │ │ │
│ │ ◄─────── unshield() ─────────── │ │
│ ▼ │ │
│ │ │
└─────────────────────────────────────────────────────────────┘
Shield (Public → Private)
What Happens
- Public ERC-20 balance decreases by amount
- Encrypted balance increases by
Enc(amount, recipientBPK) - No ZK proof needed — the user is revealing their own amount, just encrypting it
Usage
await wallet.USD.shield({ amount: 100_000000n });
await wallet.USD.shield({
amount: 100_000000n,
to: 'zk1qyp5xs...',
useRelayer: true,
});
await wallet.USD.shield({
amount: 100_000000n,
to: { x: 123n, y: 456n },
});
Registration
Registration is a one-time on-chain transaction that links your EVM address to your Balance Public Key (BPK). This is required because:
- The contract stores encrypted balances indexed by BPK, not by EVM address
- Other users need a way to look up your BPK to send you funds (via your EVM address)
- It enables autoshield — automatic conversion of incoming ERC-20 transfers into encrypted balances
The BPK does not have to be derived from the same seed as the EVM wallet — any valid Grumpkin public key can be registered against any EVM address. This enables advanced setups like institutional key management.
You must register before your first shield:
const registered = await wallet.USD.isRegistered();
if (!registered) {
await wallet.USD.registerBPK();
}
await wallet.USD.shield({ amount: 100_000000n });
Unshield (Private → Public)
What Happens
- The SDK generates a ZK proof that the user has sufficient encrypted balance
- The contract verifies the proof and replaces the encrypted balance with a new (lower) ciphertext
- Public balance increases by the unshielded amount
Usage
await wallet.USD.unshield(50_000000n);
await wallet.USD.unshield(50_000000n, '0x...recipient');
await wallet.USD.unshield(50_000000n, undefined, { useRelayer: true });
Full vs Partial Unshield
const balance = await wallet.USD.getBalance();
await wallet.USD.unshield(300_000000n);
const remaining = await wallet.USD.getBalance();
await wallet.USD.unshield(remaining);
Gas Costs
| Operation | Gas | Notes |
|---|---|---|
registerBPK() | ~50k | One-time |
shield() | ~87k | No ZK proof needed |
unshield() | ~320k | Includes ZK proof verification |
Both shield and unshield support gasless execution via relayer (EIP-2612 permit for shield, ZK proof authorization for unshield).
Shield/Unshield Amounts Are Visible
Shield and unshield amounts are visible on-chain — the contract emits events with the plaintext amount. Only private transfers (L2→L2) hide amounts.
Best Practices
- Register BPK early — Do it once at wallet setup, before first shield
- Use relayer — Users don't need ETH for gas
- Batch shields — One large shield is cheaper than many small ones
- Plan unshields — Proof generation takes time; trigger it before the user clicks "confirm"