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 (Fhenix CoFHE — encryption, decryption, permits) 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 cofhejs 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.uint128(amount)]).execute()cofhe.decryptForView(ctHash, FheTypes.Uint128).execute()cofhe.decryptForTx(handle).withoutPermit().execute()cofhe.permits.getOrCreateSelfPermit()new ethers.Contract(addr, abi, signer)End-to-end example
import { Encryptable } from "@cofhe/sdk";
import { FheTypes } from "@cofhe/sdk";
// cofhe + signer are set up as above. SealedAuction / SettlementVault are
// ethers.Contract instances built from deployed-addresses.json + the ABIs.
// 1. Fund the vault first. deposit pulls from your wallet via the FHERC-20,
// so you must authorize the vault as an operator once (FHERC20.approve
// reverts by design — use setOperator instead).
const MAX_UINT48 = "281474976710655"; // 2**48 - 1
if (!(await token.isOperator(wallet.address, vaultAddr))) {
await (await token.setOperator(vaultAddr, MAX_UINT48)).wait();
}
const [encDeposit] = (await cofhe.encryptInputs([Encryptable.uint64(100n)]).execute());
await (await vault.deposit(tokenAddr, encDeposit)).wait();
// 2. Bid on auction #4 with an encrypted price (euint128 -> Encryptable.uint128).
const [encBid] = (await cofhe.encryptInputs([Encryptable.uint128(1200n)]).execute());
const tx = await sealedAuction.bid(4, encBid);
console.log("bid tx:", tx.hash);
// 3. Later, unseal your own bid (only you can — bids are euint128).
const myBid = await cofhe.decryptForView(await sealedAuction.getMyBid(4), FheTypes.Uint128).execute();
console.log("my bid was:", myBid); // 1200nIntegration guide
The end-to-end sealed-auction lifecycle, contract calls, and event hooks.
A sealed auction goes through five states. Each transition is permissionless on the bidder side — anyone holding allowed FHERC-20 tokens can post a bid, anyone can trigger reveal once the deadline lapses.
State machine
OPENCLOSEDREVEALEDSETTLEDCANCELLEDRESERVE_NOT_METLifecycle, by contract call
// ─── Seller ───
const auctionId = await sealedAuction.createAuction(
tokenForSale, // address of the FHERC-20 being sold
paymentToken, // address of the FHERC-20 bidders pay with
amount, // plaintext amount being sold
duration, // seconds
snipeExtension, // anti-snipe extension on late bids
);
// ─── Bidder (multiple, each one independent) ───
// The InEuint128 is constructed via cofhejs with a ZK proof of validity.
const encBid = await cofhe.encryptInputs([Encryptable.uint128(1200n)]).execute();
await sealedAuction.bid(auctionId, encBid[0]);
// ─── Anyone (after deadline) ───
await sealedAuction.closeAuction(auctionId);
// Threshold network co-signs the highest-bid reveal off-chain
const proof = await cofhe.decryptForTx(highestBidHandle).withoutPermit().execute();
await sealedAuction.revealWinner(
auctionId,
proof.decryptedValue,
proof.signature,
bidderAddr,
bidderProof.signature,
);
// ─── Vault settles ───
await sealedAuction.settleAuction(auctionId);Events to subscribe to
AuctionCreatedBidPlacedAuctionClosedWinnerRevealedAuctionSettledNone 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 InEuint128 handles, not numbers. There is nothing to sandwich.
Reserve price (Blind Floor mode).
The encrypted reserve is never decrypted. The chain publishes only the boolean outcome of FHE.gte(highestBid, reserve).
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.
Decryption-on-reveal requires the FHE network to co-sign. If the network is unreachable, settlement is delayed (not lost): once a handle is FHE.allowGlobal'd at close, reveal is permissionless — anyone can fetch the co-signed result and submit revealWinner the moment the network returns. Note: the auctions have no on-chain emergency-refund. A 7-day EMERGENCY_TIMEOUT exists only on FreelanceBidding (escrowed jobs stuck in settling); do not assume it covers sealed auctions.
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:
revealWinner(auctionId, winningBid, signature, winner, bidderSig). Note the plaintextwinningBidandwinner— these were just decrypted by the threshold network and verified on-chain. - Read the SealedAuction source. Confirm that
revealWinner()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).withoutPermit().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: