Unwrapping
Two-phase unwrap flow — converting confidential tokens back to cleartext.
Unwrapping converts FHE-encrypted confidential tokens back into cleartext ERC-20 tokens. Like wrapping, it's a two-phase operation because FHE decryption happens off-chain.
Unwrap Flow
Phase 1: initUnwrap
The user calls initUnwrap with an encrypted amount, input proof, and destination address.
The wrapper:
- Caps the burn amount at the user's balance:
toBurn = FHE.min(amount, balance) - Derives the ciphertext handle:
handle = FHE.toBytes32(toBurn) - Marks
toBurnfor public decryption viaFHE.makePubliclyDecryptable - Burns
toBurnfrom the user's confidential balance - OZ stores the destination internally via
_unwrap() - Calls the unwrap-requested hook via try/catch (cannot block)
- Emits
UnwrapRequested(returns void)
Destination Locking
The destination address is stored internally by OZ at request time and cannot be changed. This prevents MEV front-running — no bot can intercept the finalization and redirect funds to a different address.
Exit Always Callable
The unwrap-requested hook uses try/catch. Even if the hook reverts, the burn and destination locking proceed. The wrapper emits UnwrapRequestedHookFailed but the unwrap continues. This guarantees that users can always exit the encrypted domain.
Phase 2: finalizeUnwrap
After off-chain decryption produces a cleartext amount and cryptographic proof, anyone can finalize the unwrap.
The wrapper:
- OZ retrieves the pre-committed destination internally
- Verifies the decryption proof via
FHE.checkSignatures - Deletes the pending unwrap record
- Calls the unwrap-finalized hook via try/catch (cannot block)
- Expands the amount back to raw ERC-20 units via
rate()anddecimals() - Transfers the underlying tokens to the destination
Permissionless Finalization
finalizeUnwrap is permissionless — anyone with a valid handle, amount, and decryption proof can call it. This enables relayer services to batch-finalize unwraps without requiring the original user to be online.
The msg.sender in the UnwrapFinalized event is the relayer, not the original requester.
initUnwrap Overloads
| Overload | Use Case |
|---|---|
initUnwrap(externalEuint64, bytes proof, address destination) | Users with FHE-encrypted input |
initUnwrap(uint64 compressedAmount, address destination) | Contracts like CardChargesEscrow that use cleartext amounts |
Key Properties
- Anti-front-running — Destination locked at request time, immutable
- Permissionless — Anyone can finalize with valid proofs
- Non-blocking exit — Unwrap hooks use try/catch, cannot prevent withdrawal
- Multiple in-flight — OZ tracks each unwrap request internally, allowing concurrent unwraps from different accounts
Was this page helpful?