RaycashDocs
Protocol

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:

  1. Caps the burn amount at the user's balance: toBurn = FHE.min(amount, balance)
  2. Derives the ciphertext handle: handle = FHE.toBytes32(toBurn)
  3. Marks toBurn for public decryption via FHE.makePubliclyDecryptable
  4. Burns toBurn from the user's confidential balance
  5. OZ stores the destination internally via _unwrap()
  6. Calls the unwrap-requested hook via try/catch (cannot block)
  7. 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:

  1. OZ retrieves the pre-committed destination internally
  2. Verifies the decryption proof via FHE.checkSignatures
  3. Deletes the pending unwrap record
  4. Calls the unwrap-finalized hook via try/catch (cannot block)
  5. Expands the amount back to raw ERC-20 units via rate() and decimals()
  6. 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

OverloadUse 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?

On this page