Skip to main content

Atomic Swaps

Exchange tokens privately between EBEMT contracts.

Overview

Atomic swaps allow two parties to exchange different tokens in a single transaction. If either transfer fails, the entire swap reverts - ensuring no one loses funds.

Alice has ebUSD, wants ebEUR
Bob has ebEUR, wants ebUSD

After swap:
- Alice has ebEUR (amount hidden)
- Bob has ebUSD (amount hidden)
- Only the swap event is visible, not the amounts

Setup

import { createClient, createSwapClient, RATE_SCALE } from '@zk-privacy/eb-sdk';
import { privateKeyToAccount } from 'viem/accounts';

// Initialize client
const client = await createClient({
chainId: 8453,
relayerUrl: 'https://eb-relayer.zkprivacy.dev',
});

// Create wallet
const wallet = client.wallet({
spendingKey: keys.spendingKey,
account: privateKeyToAccount(PRIVATE_KEY),
});

// Create swap client
const swapClient = createSwapClient({
publicClient: client.getPublicClient(),
walletClient: wallet.walletClient,
swapRegistry: '0x667d6c4d1e69399a8b881b474100dccf73ce42a0',
prover: client.getProver(),
});

How It Works

1. Maker Creates Offer

A maker advertises willingness to swap at a specific rate:

const { offerId, txHash } = await swapClient.createOffer({
tokenGive: client.getTokenAddress('USD'), // Token I'm selling
tokenReceive: client.getTokenAddress('EUR'), // Token I want
rate: 1_100_000n, // 1.1 USD per EUR (1e6 scale)
makerBPK: wallet.BPK,
});

No funds are locked. The maker just publishes their rate.

2. Both Parties Generate Proofs

Each party generates a standard transfer proof with authorizedCaller set to SwapRegistry:

// Taker: Prepare EUR transfer to maker
const takerProof = await takerWallet.EUR.prepareTransfer(
makerBPK,
eurAmount,
{ authorizedCaller: BigInt(swapClient.registryAddress) }
);

// Maker: Prepare USD transfer to taker
const makerProof = await makerWallet.USD.prepareTransfer(
takerWallet.BPK,
usdAmount,
{ authorizedCaller: BigInt(swapClient.registryAddress) }
);

3. Execute Atomic Swap

Submit both proofs in a single transaction:

function bigintToBytes32(v: bigint): `0x${string}` {
return ('0x' + v.toString(16).padStart(64, '0')) as `0x${string}`;
}

const txHash = await swapClient.executeAtomicSwap({
offerId,
takerGives: eurAmount,
takerReceives: usdAmount,
takerProof: takerProof.proofHex,
takerInputs: takerProof.publicInputs.map(bigintToBytes32),
makerProof: makerProof.proofHex,
makerInputs: makerProof.publicInputs.map(bigintToBytes32),
});

Reading Offers

// Get specific offer
const offer = await swapClient.getOffer(offerId);

// Get all active offers for a token pair
const offers = await swapClient.getActiveOffers(
client.getTokenAddress('USD'),
client.getTokenAddress('EUR')
);

// Get best offer (highest rate)
const best = await swapClient.getBestOffer(
client.getTokenAddress('USD'),
client.getTokenAddress('EUR')
);

Rate Calculations

Rates use 1e6 precision (6 decimals):

Rate ValueMeaning
1_000_0001:1 exchange
1_100_0001.1 tokenGive per tokenReceive
2_500_0002.5 tokenGive per tokenReceive
import { RATE_SCALE } from '@zk-privacy/eb-sdk';

// Calculate amounts
const receive = swapClient.calculateReceiveAmount(giveAmount, rate);
const give = swapClient.calculateGiveAmount(receiveAmount, rate);

Managing Offers

Update Rate

await swapClient.updateRate(offerId, 1_150_000n); // New rate: 1.15

Cancel Offer

await swapClient.cancelOffer(offerId);

Privacy Properties

VisibleHidden
Maker addressSwap amounts
Token pairActual balances
Exchange rateTaker identity (BPK only)
"A swap happened"How much was exchanged

Security

Proof Binding

Proofs include authorizedCaller = SwapRegistry which:

  • Prevents proof theft (can't use proof outside swap)
  • Ensures proofs only work through SwapRegistry

Atomicity

EVM transaction atomicity guarantees:

  • Both transfers succeed, OR
  • Entire transaction reverts
  • No partial execution possible

No Key Sharing

Each party uses only their own secret key:

  • Taker proves they can transfer their tokens
  • Maker proves they can transfer their tokens
  • Nobody needs anyone else's keys

Gas Costs

OperationGas
Create offer~80k
Cancel offer~30k
Atomic swap~3.4M

An atomic swap costs approximately 2x a regular private transfer, which is optimal.

Transaction History

Swaps appear in transaction history as two separate items:

const usdHistory = await wallet.USD.getHistory();
const eurHistory = await wallet.EUR.getHistory();

// USD history shows: { type: 'swap_out', amount: 110n, ... }
// EUR history shows: { type: 'swap_in', amount: 100n, ... }

Both items share the same txHash, so UI can group them as a single swap operation.

Complete Example

import { createClient, createSwapClient, keysFromMnemonic, RATE_SCALE } from '@zk-privacy/eb-sdk';
import { privateKeyToAccount } from 'viem/accounts';
import { parseUnits } from 'viem';

async function main() {
// Initialize
const client = await createClient({
chainId: 8453,
relayerUrl: 'https://eb-relayer.zkprivacy.dev',
});

// Create wallets
const aliceKeys = keysFromMnemonic(ALICE_MNEMONIC);
const alice = client.wallet({
spendingKey: aliceKeys.spendingKey,
account: privateKeyToAccount(ALICE_PK),
});

const bobKeys = keysFromMnemonic(BOB_MNEMONIC);
const bob = client.wallet({
spendingKey: bobKeys.spendingKey,
account: privateKeyToAccount(BOB_PK),
});

// Create swap client
const swapClient = createSwapClient({
publicClient: client.getPublicClient(),
walletClient: alice.walletClient,
swapRegistry: '0x667d6c4d1e69399a8b881b474100dccf73ce42a0',
prover: client.getProver(),
});

// Bob creates offer: selling USD for EUR at 1.1 rate
const { offerId } = await swapClient.createOffer({
tokenGive: client.getTokenAddress('USD'),
tokenReceive: client.getTokenAddress('EUR'),
rate: 1_100_000n,
makerBPK: bob.BPK,
});

// Alice wants to swap 100 EUR for 110 USD
const eurAmount = parseUnits('100', 6);
const usdAmount = parseUnits('110', 6);

// Alice prepares her proof (EUR → Bob)
const aliceProof = await alice.EUR.prepareTransfer(
bob.BPK,
eurAmount,
{ authorizedCaller: BigInt(swapClient.registryAddress) }
);

// Bob prepares his proof (USD → Alice)
const bobProof = await bob.USD.prepareTransfer(
alice.BPK,
usdAmount,
{ authorizedCaller: BigInt(swapClient.registryAddress) }
);

// Helper
const toBytes32 = (v: bigint) => ('0x' + v.toString(16).padStart(64, '0')) as `0x${string}`;

// Execute atomic swap
const txHash = await swapClient.executeAtomicSwap({
offerId,
takerGives: eurAmount,
takerReceives: usdAmount,
takerProof: aliceProof.proofHex,
takerInputs: aliceProof.publicInputs.map(toBytes32),
makerProof: bobProof.proofHex,
makerInputs: bobProof.publicInputs.map(toBytes32),
});

console.log('Swap executed:', txHash);

// Result:
// - Alice: -100 EUR, +110 USD
// - Bob: +100 EUR, -110 USD
// - Amounts hidden from observers

// Cleanup
await client.destroy();
}