Plug Zerith into your stack.
Everything an engineer needs to evaluate, integrate, or audit Zerith in production: the TypeScript SDK, the end-to-end sealed-auction lifecycle, what we protect (and what we don't), and a step-by-step recipe to verify any Zerith auction settled correctly using only an Etherscan link.
SDK reference
Typed TypeScript client for posting, bidding, and settling encrypted auctions.
Zerith does not ship a bespoke client. The frontend integrates directly against two installed packages: @cofhe/sdk 0.8 (Fhenix CoFHE 1.0 — encryption, decryption, access permissions) and ethers v6 (the on-chain calls, against the published contract ABIs). Every snippet below is exactly the pattern the app uses in frontend/src/providers/CofheProvider.tsx and the feature pages — copy-paste runnable against what's installed.
Install
npm install @cofhe/sdk@0.8.0 ethersInitialize the CoFHE client
import { ethers } from "ethers";
// @cofhe/sdk ships WASM — import the web entrypoints client-side only.
import { createCofheClient, createCofheConfig } from "@cofhe/sdk/web";
import { Ethers6Adapter } from "@cofhe/sdk/adapters";
import { chains } from "@cofhe/sdk/chains";
const provider = new ethers.JsonRpcProvider(SEPOLIA_RPC_URL);
const signer = new ethers.Wallet(privateKey, provider);
const cofhe = createCofheClient(
createCofheConfig({ supportedChains: [chains.sepolia] }),
);
const { publicClient, walletClient } = await Ethers6Adapter(provider, signer);
await cofhe.connect(publicClient, walletClient);Most-used methods
cofhe.encryptInputs([Encryptable.uint64(v)]).setConsumingContract(addr).execute()cofhe.decryptForView(ctHash, FheTypes.Uint64).withACP().execute()cofhe.decryptForTx(handle).withoutACP().execute()cofhe.acp.getOrCreateSelfACP()new ethers.Contract(addr, abi, signer)End-to-end example
import { Encryptable, FheTypes } from "@cofhe/sdk";
// cofhe + signer are set up as above. token / vault / sealedAuction are
// ethers.Contract instances built from deployed-addresses.json + the ABIs.
// 1. Fund your vault. Bids lock funds from this balance, so deposit first.
// Let the vault operate your ERC-7984 tokens for a short window only — an
// operator can move your whole balance (approve() reverts by design).
const until = Math.floor(Date.now() / 1000) + 600; // 10 minutes
await (await token.setOperator(vaultAddr, until)).wait();
const [depositHandle, depositProof] = await cofhe
.encryptInputs([Encryptable.uint64(500_000000n)]) // 500 tokens, 6 decimals
.setConsumingContract(vaultAddr)
.execute();
await (await vault.deposit(tokenAddr, depositHandle, depositProof)).wait();
// 2. Bid on auction #0. The amount is locked from your vault balance;
// if you can't cover it, nothing is locked and the bid can't win.
const [bidHandle, bidProof] = await cofhe
.encryptInputs([Encryptable.uint64(120_000000n)])
.setConsumingContract(auctionAddr)
.execute();
await (await sealedAuction.bid(0, bidHandle, bidProof)).wait();
// 3. Unseal your own bid (the amount actually locked). Only you can.
await cofhe.acp.getOrCreateSelfACP();
const myBid = await cofhe
.decryptForView(BigInt(await sealedAuction.getMyBid(0)), FheTypes.Uint64)
.withACP()
.execute();Integration guide
The end-to-end sealed-auction lifecycle, contract calls, and event hooks.
Every bid and the seller's lot are locked in the settlement vault, so a sale settles atomically from escrow: the winner always pays exactly the winning bid and always receives the lot. After creation, every step except cancelling can be triggered by anyone.
State machine
PENDING_LOTOPENCLOSEDREVEALEDSETTLEDCANCELLEDNO_SALELifecycle, by contract call
// ─── Seller ─── (deposit the lot into the vault first)
await sealedAuction.createAuction(lotToken, paymentToken, lotAmount, duration, snipeExtension);
// Anyone: publish the Threshold Network's check that the lot was fully locked
const lot = await cofhe.decryptForTx(await sealedAuction.getLotFundedHandle(id)).withoutACP().execute();
await sealedAuction.confirmLot(id, lot.decryptedValue === 1n, lot.signature);
// ─── Bidders ─── (each bid locks funds from the bidder's vault balance)
const [h, proof] = await cofhe.encryptInputs([Encryptable.uint64(amount)])
.setConsumingContract(auctionAddr).execute();
await sealedAuction.bid(id, h, proof);
// ─── Anyone, after the deadline ───
await sealedAuction.closeAuction(id);
const [okH, bidH, winnerH] = await sealedAuction.getRevealHandles(id);
const ok = await cofhe.decryptForTx(okH).withoutACP().execute();
const bid = await cofhe.decryptForTx(bidH).withoutACP().execute();
const win = await cofhe.decryptForTx(winnerH).withoutACP().execute();
await sealedAuction.reveal(id, ok.decryptedValue === 1n, ok.signature,
bid.decryptedValue, bid.signature, toAddress(win.decryptedValue), win.signature);
await sealedAuction.settleAuction(id);
// ─── Each losing bidder ─── (refund stays encrypted; it is never decrypted)
await sealedAuction.claimRefund(id);Events to subscribe to
AuctionCreatedLotConfirmedBidPlacedAuctionClosedWinnerRevealedNoSaleAuctionSettledRefundClaimedNone of these events leak losing-bidder information. The chain stores the full set of encrypted bids forever, but they remain cryptographically inaccessible.
Threat model
What Zerith protects against, what it does not.
What we protect
Losing bid amounts.
Encrypted on-chain forever. No FHE.allowGlobal is ever called on losing handles. Even the protocol deployer cannot decrypt them.
Cross-account decryption.
The threshold network refuses requests where the caller doesn't own the handle. Bidder #2 cannot unseal Bidder #1's bid.
Pre-trade leakage to MEV.
Bid amounts are ciphertext at submission. Searchers see encrypted input handles, not numbers. There is nothing to sandwich.
Reserve price (Blind Floor mode).
The encrypted reserve is never decrypted. The chain publishes only whether the sale went through, and if it didn't, neither the winner nor the winning bid is revealed.
What we do NOT protect (yet)
Bidder identity.
Bidder addresses are public on-chain. To anonymize who bid, route through a fresh wallet (the burner flow) or a privacy mixer at the transaction level. We do not bundle that.
Auction existence + size.
The fact that an auction was posted, by whom, for what token quantity, on what deadline — all public. We seal the prices, not the trade itself. This is intentional: foundations want their sale to be discoverable.
Threshold network availability.
Lot confirmation and the reveal need the Threshold Network to co-sign. If it's unreachable, the sale is delayed, not lost: closing and revealing are open to anyone and can be retried the moment the network returns. While it's down, escrowed bids and the lot stay locked; there is no on-chain timeout that refunds them without a reveal.
On-chain censorship / mempool ordering.
MEV searchers cannot front-run the bid (they can't read it), but they can reorder transactions inside a block. For sealed auctions this only affects bid arrival timing within anti-snipe windows.
Cryptographic assumptions
Verify a settlement
From an Etherscan link, prove a Zerith auction cleared correctly.
This is the load-bearing check. Anyone with an Etherscan link can verify a Zerith auction settled correctly without trusting us, without running a node, without an account.
The headline tx
The canonical example we point reviewers at:
Step-by-step
- Open the reveal tx on Etherscan. Confirm
To:matches the SealedAuction address in our deployed-addresses.json. - Decode the input data. Etherscan's "Decode Input Data" reveals the function call:
reveal(auctionId, saleOk, sig, winningBid, sig, winner, sig). Note the plaintextwinningBidandwinner— these were just decrypted by the threshold network and verified on-chain. - Read the SealedAuction source. It's verified on Etherscan, so you can check it matches this repository. Confirm that
reveal()callsFHE.publishDecryptResult(handle, value, signature)— this is the precompile that verifies the threshold network signature against the ciphertext handle. If it returnsfalse, the call reverts. - Read the storage slot for losing bids.
bids[auctionId][bidderAddr]stores the encrypted handle for every bidder. Anyone can read these slots; nobody can decrypt them. The losing bid handles for this auction sit in storage forever. - Try to decrypt a losing bid yourself. Call
cofhe.decryptForTx(handle).withoutACP().execute()from a wallet that didn't place that bid. The threshold network refuses — we never calledFHE.allowGlobalon losing handles, so they remain owner-restricted.
Spot-check the math
For the headline tx specifically: