Market making with a DFNS wallet
You can run a market maker without ever holding a private key. Instead of an EOA, the maker identity is a DFNS-custodied wallet. DFNS holds the key, signs your transactions, and (once you delegate) decrypts your own confidential inventory for you. The protocol lifecycle is unchanged; only who holds the key and how decryption happens differ. This follows DFNS's confidential-tokens (ERC-7984) flow.
This page is the DFNS variant of the Market makers guide. Read that one first for the auction, the SDK flow, and the settlement rules. Here we only cover what changes when DFNS is your wallet.
The wallet is your maker address
The DFNS wallet address is your maker address (me in the SDK-flow
snippets). It is the address that:
- receives
MARKET_MAKER_ROLEfrom an admin, - is granted operator authority via
setOperator(swap, expiry), - holds confidential inventory on both legs of each pair, and
- is named as
delegatein each intent'smarketMakerDecryptionAccess.
Grant the role and operator authority to that address, exactly as you would an EOA. Nothing in the contract knows or cares that a custodian holds the key.
Setup
Create a DFNS service account (generate a keypair, register the public key in the DFNS dashboard) and a DFNS EVM wallet, then point the maker at it:
DFNS_API_URL = https://api.dfns.io # https://api.dfns.ninja for the staging env
DFNS_ORG_ID = <org id from the dashboard>
DFNS_CRED_ID = <service-account signing-key cred id>
DFNS_PRIVATE_KEY = <service-account private key>
DFNS_AUTH_TOKEN = <service-account auth token>
WALLET_ID = <DFNS wallet id> # its address is your maker addressGrant the service account its permissions. A fresh service account can do nothing — every API call 403s until you assign it a permission set. In the DFNS dashboard (Org → Permissions → New role), select exactly two operations and assign the role to the service account:
Wallets:Read— fetch the wallet's address/metadata at startup.Keys:Signatures:Create— generate signatures (every transaction and every EIP-712 the maker signs). Note this lives under Keys, not Wallets: DFNS's permission taxonomy has noWallets:GenerateSignatureoperation.
Nothing else is needed — the maker broadcasts through its own RPC, so no transaction/transfer permissions.
Fund the wallet with gas. DFNS signs and broadcasts, but you still pay for your own transactions.
1. Optional: delegate inventory decryption to DFNS (once per token)
You do not need this step to read your own balances: the standard
sdk.decryption.decryptValues(...) flow works unchanged with a DFNS wallet —
the EIP-712 permit it needs is simply signed through DFNS like every other
signature. Skip ahead to step 2 if that's enough.
The alternative below moves balance reads to DFNS's own API instead (useful
if your ops tooling already lives in the DFNS dashboard): call
delegateForUserDecryption on the Zama ACL from your DFNS wallet, naming the
DFNS delegatee. This is a one-time transaction per wallet per token contract
(default expiry: 1 year):
| Sepolia | Mainnet | |
|---|---|---|
| Zama ACL | 0xf0Ffdc93b7E186bC2f8CB3dAA75D86d1930A433D | 0xf0Ffdc93b7E186bC2f8CB3dAA75D86d1930A433D |
| DFNS delegatee | 0x1f4252accc541a7a37868e02031b85ea00245d73 | 0x1f4252accc541a7a37868e02031b85ea00245d73 |
// ACL.delegateForUserDecryption(delegate, contractAddress, expirationDate)
const data = encodeFunctionData({
abi: aclAbi,
functionName: "delegateForUserDecryption",
args: [DFNS_DELEGATEE, TOKEN, expirationUnixTs],
});
await dfns.wallets.broadcastTransaction({
walletId: WALLET_ID,
body: { kind: "Transaction", transaction: serializeTransaction({ to: ACL, data /* ... */ }) },
});If you adopt this flow, do it for both legs of every pair you quote. Once
confirmed, DFNS's Get Wallet Assets and Get Wallet History endpoints return
your balances in plaintext, which can stand in for
sdk.decryption.decryptValues(...) in the settlement preflight and ongoing
inventory checks.
2. Sign and broadcast protocol txs through DFNS
quoteIntent, resolveQuoting, settleIntent, setOperator, and inventory
wrapping all go through DFNS's Broadcast Transaction endpoint
(kind: "Transaction") instead of a local signer. Wire the DFNS wallet in as
your viem walletClient (via the @dfns/sdk
viem adapter) and the SDK-flow snippets from the Market makers guide are
unchanged:
const sdk = new ZamaSDK(
createConfig({
chains: [{ ...sepolia, network: RPC_URL, relayerUrl: RELAYER_URL }],
relayers: { [sepolia.id]: node() },
publicClient,
walletClient: dfnsWalletClient,
storage: new MemoryStorage(),
}),
);For fee-sponsored sends, DFNS exposes kind: "UserOperations" with a fee sponsor
in place of kind: "Transaction".
3. Per-intent decryption still goes through the Zama relayer
DFNS's managed decryption covers your own balances, not the protocol's
per-intent delegation. Decrypting an intent's direction and input amount is still
the SDK's delegatedDecryptValues, whose delegation flows from SWAP to your
maker address through marketMakerDecryptionAccess (see the Market makers
SDK flow). The delegated EIP-712 permit it needs must be
signed by your DFNS wallet (the delegate) — with the DFNS-backed
walletClient wired in as the SDK's signer (step 2), the SDK produces that
signature automatically through DFNS's Generate Signature endpoint
(kind: "Eip712"); no manual key-pair or signature plumbing:
const mmda = intent.marketMakerDecryptionAccess;
const clear = await sdk.decryption.delegatedDecryptValues(
[
{ encryptedValue: intent.eFromAsset0To1, contractAddress: mmda },
{ encryptedValue: intent.eAmountIn, contractAddress: mmda },
],
SWAP, // the delegator; the delegate is the SDK's connected signer (your DFNS wallet)
);Operational notes
- Latency budget. Every DFNS signature and broadcast is a network round-trip.
Decrypt → price → encrypt → sign → broadcast must still all land before
quotingEndsAtBlock, so budget for the extra hops and keep the SDK warm. - Nonces. The wallet's nonce is single-threaded just like an EOA, so serialize quotes, resolves, and settles through one wallet.
- Settlement preflight. Same rule as the EOA flow: only quote a pair you can
settle (role, both-leg initialization + delegation, operator approval,
inventory). Read inventory via
sdk.decryption.decryptValues(signed through DFNS), or from DFNS's plaintext balances if you set up the optional delegation in step 1.