— Integration docs · for engineers

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.

— Contents
  1. 01SDK reference
  2. 02Integration guide
  3. 03Threat model
  4. 04Verify a settlement
Section 01

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

bash
npm install @cofhe/sdk cofhejs ethers

Initialize the CoFHE client

ts
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()
Encrypt a value client-side into an InEuint128 (with a ZK proof of validity). Pass the returned handle to the contract call. Use Encryptable.uint64(...) for euint64 amounts.
cofhe.decryptForView(ctHash, FheTypes.Uint128).execute()
Decrypt your own handle locally via the threshold network, gated by your permit. Cross-account calls are rejected by the TN — you can only unseal handles your address owns.
cofhe.decryptForTx(handle).withoutPermit().execute()
Fetch a TN co-signed (value, signature) pair for an FHE.allowGlobal'd handle, then submit it on-chain (e.g. revealWinner). Permissionless once the handle is globally allowed.
cofhe.permits.getOrCreateSelfPermit()
Idempotently ensure an active self-permit exists. Permits gate decryptForView; they last ~24h and auto-rotate in the app.
new ethers.Contract(addr, abi, signer)
Plain ethers v6 contract instance for the on-chain calls — createAuction, bid, closeAuction, revealWinner, vault.deposit. Addresses live in deployed-addresses.json.

End-to-end example

ts
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); // 1200n
Section 02

Integration 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

OPEN
Created. Accepts bids until the deadline.
CLOSED
Deadline passed or seller closed early. No more bids accepted.
REVEALED
Threshold network co-signed the winning bid + bidder. Plaintext recorded.
SETTLED
Settlement vault transferred tokens between winner and seller.
CANCELLED
Seller cancelled before any bids landed.
RESERVE_NOT_MET
Blind Floor only — the encrypted reserve check returned false at reveal. Seller refunded; bidders refunded.

Lifecycle, by contract call

ts
// ─── 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

AuctionCreated
auctionId, seller, token, amount, deadline
BidPlaced
auctionId, bidder, newDeadline (anti-snipe extension)
AuctionClosed
auctionId
WinnerRevealed
auctionId, winner, winningBid (plaintext)
AuctionSettled
auctionId — vault transfer complete

None of these events leak losing-bidder information. The chain stores the full set of encrypted bids forever, but they remain cryptographically inaccessible.

Section 03

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

FHE security
Fhenix CoFHE uses TFHE-rs. Security reduces to standard learning-with-errors hardness assumptions. We rely on the upstream library; we do not roll our own crypto.
Threshold network
Reveals require a quorum of independent operators to co-sign. Compromise of fewer than the threshold leaks nothing. Compromise of the threshold compromises decryption (but not the encryption — past auctions stay sealed).
Permits
Each user holds a 30-day permit signed once. Permits gate decryption-on-view (own balance, own bid). Compromise of a permit allows the attacker to decrypt only what that user is allowed to see.
Section 04

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:

— EtherscanSealed auction · 3 bidders · winner revealed0x98a1c650b8f992dacba8580ac25aa1c1960bde1d37fa490697a9a143014fafc7

Step-by-step

  1. Open the reveal tx on Etherscan. Confirm To: matches the SealedAuction address in our deployed-addresses.json.
  2. Decode the input data. Etherscan's "Decode Input Data" reveals the function call: revealWinner(auctionId, winningBid, signature, winner, bidderSig). Note the plaintext winningBid and winner — these were just decrypted by the threshold network and verified on-chain.
  3. Read the SealedAuction source. Confirm that revealWinner() calls FHE.publishDecryptResult(handle, value, signature) — this is the precompile that verifies the threshold network signature against the ciphertext handle. If it returns false, the call reverts.
  4. 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.
  5. 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 called FHE.allowGlobal on losing handles, so they remain owner-restricted.

Spot-check the math

For the headline tx specifically:

Bidders
burner1 (500), burner2 (800), burner3 (1200)
Winner revealed
burner3 / 1200 ✓ matches highest bid
Losing bids in storage
burner1 / burner2 — encrypted handles, never decrypted
Verify yourself

— Need more depth

Source-level documentation lives in the repo. The full reviewer replay path, every verified Sepolia transaction, and the launch QA results are linked from the README.