Raycash Docs
Guides

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:

  1. Encrypt — Backend encrypts the recipient address via the Zama FHE SDK
  2. Predict — Compute the CREATE2 depositor address from the encrypted handle
  3. Send — User transfers ERC-20 tokens to that address
  4. 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 salt

Each 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:

  1. Verifies the FHE input (FHE.fromExternal) and stores the encrypted recipient handle
  2. Deploys a RaycashDepositor at the CREATE2 address (reuses existing depositor if already deployed)
  3. Calls sweep() on the depositor to transfer tokens to the wrapper (only the wrapper can call sweep())
  4. Compresses the amount to euint64 and 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

ErrorCauseResolution
ZeroAmountparams.amount is zeroSupply a non-zero amount in DeployParams
TransferHookRevertedTransfer hook rejected the mint during finalizeWrapEnsure recipient has valid KYC attestation
TooFewDecoysNot enough indices in finalizeWrapInclude at least minDecoys deposit indices
InvalidCommitmentSecret does not match the committed hash for the depositorVerify the correct secret is being used

Was this page helpful?

On this page