Skip to main content
ConfidentialPaymentRouterV2 lets you send any ERC-20 token where the transfer amount is hidden on Ethereum. You pay with plain ERC-20; the router wraps it into a cToken, transfers it encrypted, then unwraps it — the receiver gets standard ERC-20 with no FHE interaction required. Mainnet address: 0x087D50Bb21a4C7A5E9394E9739809cB3AA6576Fa

Constants

ConstantValue
MIN_RELAYER_FEE0.00005 ETH
PAYMENT_TIMEOUT1 day

Payment flow

Sender: approve(token, router, amount)
  → send(token, receiver, amount, memo) + MIN_RELAYER_FEE in ETH
    → router pulls ERC-20 from sender
    → router wraps ERC-20 into cToken (encrypted)
    → router requests unwrap (KMS decryption triggered)

Relayer: finalize(paymentId, handlesList, cleartexts, decryptionProof)
  → KMS proof verified on-chain
  → plain ERC-20 transferred to receiver
  → relayer receives ETH fee

send

function send(
    address token,
    address receiver,
    uint256 amount,
    string calldata memo
) external payable returns (uint256 paymentId)
Initiate a confidential payment. You must approve the router for amount of token before calling, and attach at least MIN_RELAYER_FEE in ETH.
token
address
required
The ERC-20 token to send. A wrapper must exist in WrapperFactory (or the router will revert with WrapperNotFound).
receiver
address
required
Who receives the plain ERC-20 after finalization.
amount
uint256
required
Amount in the token’s native decimals.
memo
string
required
Human-readable note attached to the payment.
Returns a paymentId used to track and finalize the payment. Estimated gas: ~1,500,000.

finalize

function finalize(
    uint256 paymentId,
    bytes32[] calldata handlesList,
    bytes calldata cleartexts,
    bytes calldata decryptionProof
) external
Submit the KMS decryption proof to complete the payment. Permissionless — anyone can call this, but the relayer is incentivized by the ETH fee.
paymentId
uint256
required
The payment ID returned by send().
handlesList
bytes32[]
required
FHE ciphertext handles decrypted by the KMS.
cleartexts
bytes
required
ABI-encoded decrypted values (the canUnwrap boolean).
decryptionProof
bytes
required
Threshold KMS signatures verified via FHE.checkSignatures().
On success, the plain ERC-20 is transferred to the receiver, the payment is marked finalized, and the relayer receives the ETH fee.

cancel

function cancel(uint256 paymentId) external
Cancel a payment after PAYMENT_TIMEOUT (1 day) if it has not been finalized. Only the original sender can cancel. The ERC-20 is returned and the ETH fee refunded.

getPayment

function getPayment(uint256 paymentId) external view returns (
    address sender, address receiver, address wrapper,
    uint64 amount, uint256 unwrapRequestId, bytes32 handle,
    uint256 relayerFee, string memory memo, uint256 createdAt,
    bool finalized, bool cancelled
)

Payment struct fields

sender
address
Address that initiated the payment.
receiver
address
Address that will receive the plain ERC-20.
wrapper
address
The ConfidentialWrapper used for this payment.
amount
uint64
Amount in 6-decimal cToken units.
unwrapRequestId
uint256
The unwrap request ID in the ConfidentialWrapper contract.
handle
bytes32
The ebool ciphertext handle for canUnwrap.
relayerFee
uint256
ETH fee paid to the relayer on finalization.
memo
string
Human-readable note.
createdAt
uint256
Block timestamp of payment creation.
finalized
bool
Whether the payment has been finalized.
cancelled
bool
Whether the payment was cancelled.

Events

EventParameters
PaymentCreatedpaymentId, sender, receiver, wrapper, amount, memo
PaymentUnwrapRequestedpaymentId, unwrapRequestId, handle
PaymentFinalizedpaymentId, receiver, amount, success
PaymentCancelledpaymentId, sender

Custom errors

ErrorWhen thrown
ZeroAddressreceiver is address(0)
ZeroAmountamount is 0
InsufficientRelayerFeemsg.value < MIN_RELAYER_FEE
PaymentNotFoundpaymentId does not exist
PaymentAlreadyFinalizedPayment already finalized or cancelled
WrapperNotFoundNo wrapper exists for the given token
NotSenderCaller is not the payment sender (for cancel)
TooEarlyTimeout not yet elapsed (for cancel)

Confidential Payments

How to use confidential payments from the frontend.

ConfidentialWrapper

The wrapper contract used internally by the router.

Build docs developers (and LLMs) love