Market makers
ConfidentialSwap runs a sealed-bid auction per intent: a user locks an
encrypted amount and a hidden slippage floor; market makers compete to offer the
best output; the highest valid quote wins and settles. As a market maker you run
an off-chain agent that watches new intents (IntentCreated), decrypts
each one's direction and input amount, prices and submits an encrypted
quote (quoteIntent), resolves the auction after the window
(resolveQuoting), and settles the intents it wins (settleIntent).
The only hard dependencies are the protocol contract, an EVM client, and the
Zama SDK (@zama-fhe/sdk).
Everything else (RPC host, event source, pricing, persistence) is your choice.
What you can and cannot see
| Field | Visible to you? |
|---|---|
Trade direction (eFromAsset0To1) | yes, via delegated decrypt |
Input amount (eAmountIn) | yes, via delegated decrypt |
User's slippage floor (eAmountOutMin) | no, user-only |
Other makers' quotes (eAmountOutBest) | no, encrypted |
Winner (eBestQuoter), after resolveQuoting | yes, publicly decryptable |
You quote blind to both the floor and your competitors: to win, your quote must be the highest and at or above the hidden floor. Your liquidity operations are public on-chain (wrapping, which assets you hold), so keep inventory pre-funded and balanced across both legs rather than topping up per trade; reactive provisioning leaks the trade by correlation.
Prerequisites
- An EOA funded with gas; it signs decryption permits and your txs.
MARKET_MAKER_ROLE, granted by an admin (this auto-wires your delegated decryption rights; you can only decrypt intents created after you joined).- Confidential inventory initialized on both legs of each pair you quote (settlement does two transfers, one real and one zero, to hide direction).
- Operator authority for the swap contract on each token
(
setOperator(swap, expiry)), so it can pull your output at settlement.
Run a startup preflight that checks role, both-leg initialization, and operator approval, and refuses to quote a pair it cannot settle.
Lifecycle
user --> createIntent (locks encrypted amount + floor)
| -- QUOTING -- (until quotingEndsAtBlock)
maker (you) --> quoteIntent (block <= quotingEndsAtBlock)
anyone --> resolveQuoting (block > quotingEndsAtBlock, once; reveals winner)
| -- SETTLEMENT -- (until settlementEndsAtBlock)
winning maker --> settleIntent (block <= settlementEndsAtBlock)
anyone --> reclaimExpiredIntent (if unfilled -> funds back to the user)Events: IntentCreated (your trigger; carries marketMakerDecryptionAccess, the
per-intent FHE delegation context), IntentResolved (winner now publicly
decryptable), IntentQuoted, IntentSettled, IntentReclaimed.
SDK flow
Construct the SDK once and reuse it (the keyset downloads lazily on first use):
import { MemoryStorage, ZamaSDK } from "@zama-fhe/sdk";
import { sepolia } from "@zama-fhe/sdk/chains";
import { node } from "@zama-fhe/sdk/node";
import { createConfig } from "@zama-fhe/sdk/viem"; // or from "@zama-fhe/sdk/ethers"
const sdk = new ZamaSDK(
createConfig({
chains: [{ ...sepolia, network: RPC_URL, relayerUrl: RELAYER_URL }],
relayers: { [sepolia.id]: node() }, // Node FHE backend (worker-thread WASM pool)
publicClient, // your existing viem clients
walletClient,
storage: new MemoryStorage(), // persist (e.g. redis) to cache permits across restarts
}),
);There are four FHE operations across the lifecycle.
1. Decrypt the intent inputs. Delegated; rights flow through the per-intent
marketMakerDecryptionAccess context from the event (use that one, never a cached
context):
const mmda = intent.marketMakerDecryptionAccess;
// The SDK reproduces the delegation tuple (delegator = swap, context = mmda,
// delegate = you) — the delegate is its connected signer, and the transport
// key pair + delegated EIP-712 permit are signed and cached internally:
const clear = await sdk.decryption.delegatedDecryptValues(
[
{ encryptedValue: intent.eFromAsset0To1, contractAddress: mmda },
{ encryptedValue: intent.eAmountIn, contractAddress: mmda },
],
SWAP, // the delegator
);
const direction = Boolean(clear[intent.eFromAsset0To1]);
const amountIn = clear[intent.eAmountIn] as bigint; // 0n if the user's deposit failedYou cannot decrypt eAmountOutMin (the floor); quote without it.
2. Encrypt + submit your quote. The gross output, before
quotingEndsAtBlock:
// Both fields come back as contract-ready hex.
const { encryptedValues, inputProof } = await sdk.encrypt({
contractAddress: SWAP,
userAddress: me,
values: [{ value: quoteAmount, type: "euint64" }],
});
await walletClient.writeContract({
address: SWAP, abi, functionName: "quoteIntent",
args: [intentId, encryptedValues[0], inputProof],
});The per-pair fee (quote >> feeShift) is skimmed from your quote to the
protocol; the user receives the remainder, so your outlay is exactly your quote.
You can submit multiple quotes for an intent: the contract keeps the running maximum, so only the highest quote ever wins and a lower re-quote can't lower your standing. Quotes cannot be cancelled once submitted.
3. Resolve after the quoting window (permissionless; reverts if already done):
await walletClient.writeContract({ address: SWAP, abi, functionName: "resolveQuoting", args: [intentId] });4. Detect a win and settle. The winner is publicly decryptable after resolve:
const { clearValues } = await sdk.decryption.decryptPublicValues([eBestQuoter]);
if ((clearValues[eBestQuoter] as string).toLowerCase() === me.toLowerCase()) {
await walletClient.writeContract({ address: SWAP, abi, functionName: "settleIntent", args: [intentId] });
}Settle reverts without operator authority or after the window; it transfers
nothing if you're short on liquidity (confidential transfers clamp to zero
rather than revert). For inventory checks, decrypt your own balances with
sdk.decryption.decryptValues([{ encryptedValue, contractAddress }, ...]).
See Contract interface & permissions for every entry point with its caller gate and timing, the role model, and the errors worth handling.
Operational notes
- Window budget. Decrypt → price → encrypt → sign → broadcast must all fit
before
quotingEndsAtBlock; encryption (proof generation) and a cold keyset download are the slow steps, so keep the SDK warm. - Idempotency. Event sources redeliver and reorgs replay; guard against
double-quoting (check for your own
IntentQuoted).resolveQuotingandsettleIntentself-guard by reverting. - Nonces. One EOA submits quotes, resolves, and settles, so serialize sends.
- Settlement preflight. Only quote a pair you can settle (role, both-leg initialization, operator approval, inventory); a won-but-unsettleable quote earns nothing and wastes the user's window.