Integrating Deposits
Step-by-step guide for integrating counterfactual deposits into your application.
This guide walks through integrating counterfactual deposits — the process of allowing users to deposit tokens by sending them to a pre-computed address.
Overview
The deposit flow has four stages:
- Encrypt — Backend encrypts the recipient address via the Zama FHE SDK
- Predict — Compute the CREATE2 depositor address from the encrypted handle
- Send — User transfers ERC-20 tokens to that address
- Deploy — Operator calls
wrapper.initWrap(), which deploys the depositor, sweeps tokens, and records the deposit
Step 1: Encrypt the Recipient
The backend encrypts the recipient address using the Zama FHE SDK. The encrypted handle serves double duty: it is both the FHE ciphertext for the recipient and the CREATE2 salt for the depositor address.
import { createInstance } from "fhevmjs";
const fhevm = await createInstance({
networkUrl: rpcUrl,
gatewayUrl: gatewayUrl,
});
const input = fhevm.createEncryptedInput(wrapperAddress, userAddress);
input.addAddress(recipientAddress);
const { handles, inputProof } = input.encrypt();
const encryptedRecipient = handles[0]; // bytes32 — the CREATE2 saltEach encryption produces unique randomness, so every deposit gets a unique depositor address even for the same recipient.
Step 2: Predict the Depositor Address
CREATE2 addresses depend on the deployer (the wrapper), the salt (the encrypted recipient handle), and the init code hash (depositor bytecode + wrapper address):
// Off-chain: compute the CREATE2 address
const depositorAddress = computeDepositorAddress({ wrapperAddress, salt: encryptedRecipient });The canonical off-chain implementation lives in src/utils/compute-depositor-address.ts. It computes the CREATE2 address from (wrapper, salt, initCodeHash).
Step 3: User Sends Tokens
The user performs a standard ERC-20 transfer to the predicted address:
await underlyingToken.write.transfer([depositorAddress, amount]);No contract exists at this address yet — the tokens sit at the predicted CREATE2 address. No approval is required.
Step 4: Deploy the Depositor
The wrapper operator (OPERATOR_ROLE) calls initWrap() with the encrypted recipient, input proof, and expected amount:
await wrapper.write.initWrap([{
encryptedRecipient, // externalEaddress — FHE-encrypted recipient, also the CREATE2 salt
inputProof, // ZKP input proof bound to the wrapper address
amount, // amount to sweep (depositor must hold at least this much)
}]);The wrapper:
- Verifies the FHE input (
FHE.fromExternal) and stores the encrypted recipient handle - Deploys a RaycashDepositor at the CREATE2 address (reuses existing depositor if already deployed)
- Calls
sweep()on the depositor to transfer tokens to the wrapper (only the wrapper can callsweep()) - Compresses the amount to
euint64and records the deposit
Batch Deploys
The wrapper also supports batch deploys for processing multiple deposits in a single transaction:
await wrapper.write.initWrap([
[
{ encryptedRecipient: handle1, inputProof: proof1, amount: amount1 },
{ encryptedRecipient: handle2, inputProof: proof2, amount: amount2 },
],
]);Step 5: Finalize the Wrap
The recipient (or anyone on their behalf) calls finalizeWrap on the wrapper with a batch of deposit indices (including decoys) and the recipient address:
await wrapper.write.finalizeWrap([depositIndices, recipientAddress]);The wrapper uses FHE.eq to match the recipient against each deposit's encrypted recipient. Non-matching deposits contribute zero — an observer cannot tell which deposits were claimed.
Rage Quit (Safety Valve)
If the operator has not called initWrap() for a funded depositor address, the intended recipient can exit without the operator's cooperation using a single-transaction hash-preimage withdrawal:
// Withdraw tokens from depositor using the hash preimage.
// Tokens go directly to the recipient without touching the wrapper's balance.
await wrapper.write.withdraw([depositorAddr, secret, recipientAddr, tokenAddr, handle]);This works for both undeployed and already-deployed depositors. The caller provides a secret whose hash matches the committed hash for that depositor.
Error Handling
| Error | Cause | Resolution |
|---|---|---|
ZeroAmount | params.amount is zero | Supply a non-zero amount in DeployParams |
TransferHookReverted | Transfer hook rejected the mint during finalizeWrap | Ensure recipient has valid KYC attestation |
TooFewDecoys | Not enough indices in finalizeWrap | Include at least minDecoys deposit indices |
InvalidCommitment | Secret does not match the committed hash for the depositor | Verify the correct secret is being used |
Was this page helpful?