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 Value | Meaning |
|---|---|
1_000_000 | 1:1 exchange |
1_100_000 | 1.1 tokenGive per tokenReceive |
2_500_000 | 2.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
| Visible | Hidden |
|---|---|
| Maker address | Swap amounts |
| Token pair | Actual balances |
| Exchange rate | Taker 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
| Operation | Gas |
|---|---|
| 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();
}