Skip to main content

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

  1. Public ERC-20 balance decreases by amount
  2. Encrypted balance increases by Enc(amount, recipientBPK)
  3. 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

  1. The SDK generates a ZK proof that the user has sufficient encrypted balance
  2. The contract verifies the proof and replaces the encrypted balance with a new (lower) ciphertext
  3. 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

OperationGasNotes
registerBPK()~50kOne-time
shield()~87kNo ZK proof needed
unshield()~320kIncludes 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

  1. Register BPK early — Do it once at wallet setup, before first shield
  2. Use relayer — Users don't need ETH for gas
  3. Batch shields — One large shield is cheaper than many small ones
  4. Plan unshields — Proof generation takes time; trigger it before the user clicks "confirm"