Confidential Transfer
A confidential transfer moves wrapped tokens between accounts with the amount encrypted end-to-end. Observers (explorers, indexers, the recipient’s neighbors) see that a transfer happened between two addresses, but never how much.
Snippets assume Getting Started plus the SDK singleton from the Decryption Model. Add the transfer entries to your wrapper ABI:
export const transferAbi = parseAbi([
"function confidentialTransfer(address to, bytes32 encryptedAmount, bytes inputProof) returns (bytes32)",
"function confidentialTransferFrom(address from, address to, bytes32 encryptedAmount, bytes inputProof) returns (bytes32)",
"function setOperator(address operator, uint48 until)",
"function isOperator(address holder, address spender) view returns (bool)",
"event ConfidentialTransfer(address indexed from, address indexed to, bytes32 indexed amount)",
]);Send
Exactly like the unwrap request, the amount is encrypted client-side into an
input bound to (wrapper, sender), with a proof the contract verifies:
import { toHex } from "viem";
import { getFhevmInstance } from "./fhevm";
const instance = await getFhevmInstance();
const input = instance.createEncryptedInput(wrapper, account);
input.add64(amountWrapperUnits); // 6-decimal wrapper base units
const { handles, inputProof } = await input.encrypt();
const hash = await walletClient.writeContract({
address: wrapper,
abi: transferAbi,
functionName: "confidentialTransfer",
args: [recipient, toHex(handles[0]), toHex(inputProof)],
account,
});
await publicClient.waitForTransactionReceipt({ hash });The recipient needs no setup: their balance is just a new ciphertext handle they can decrypt whenever they choose.
Insufficient balance does not revert. The contract cannot branch on an
encrypted comparison, so sending more than you hold transfers an encrypted
zero instead of failing. The function’s euint64 return handle is the
actual transferred amount. Check the sender’s decrypted balance in your
UI before submitting, because the chain will not stop you.
What the chain sees
event ConfidentialTransfer(address indexed from, address indexed to, euint64 indexed amount);amount is a ciphertext handle, which is why Obscura’s public
activity feed can show wrap amounts (public by nature)
but only the fact of confidential transfers.
Delegating with operators
ERC-7984’s allowance analogue is the operator: an address authorized to move your wrapped tokens until an expiry timestamp.
// Grant for 24 hours
const until = Math.floor(Date.now() / 1000) + 24 * 60 * 60;
await walletClient.writeContract({
address: wrapper,
abi: transferAbi,
functionName: "setOperator",
args: [operatorAddress, until],
account,
});
// The operator can then call:
// confidentialTransferFrom(holder, to, encryptedAmount, inputProof)Revoke by setting until to a past timestamp (e.g. 0). Unlike ERC-20
allowances, operator grants are all-or-nothing per address but expire
automatically, so prefer short windows.
The wrapper’s own unwrap(from, to, …) uses the same mechanism: a
contract or account unwrapping on someone’s behalf must be their operator.
When from === msg.sender, no operator grant is needed.